Server : Apache System : Linux server1.cgrithy.com 3.10.0-1160.95.1.el7.x86_64 #1 SMP Mon Jul 24 13:59:37 UTC 2023 x86_64 User : nobody ( 99) PHP Version : 8.1.23 Disable Function : NONE Directory : /etc/ |
=encoding utf-8 =head1 NAME /etc/exim.pl.local - Perl functions for exim that are loaded by /etc/exim.pl =cut my $VALIASES_DIR = '/etc/valiases'; my $VDOMAINALIASES_DIR = '/etc/vdomainaliases'; my $outgoing_mail_suspended_message; my $outgoing_sender; my $outgoing_sender_domain; my $outgoing_sender_counted_domain; my $outgoing_sender_sysuser; my $outgoing_sender_is_mailman; my $outgoing_sender_archive_directory = 'outgoing'; my $mail_gid; my $nobody_uid; my $nobody_gid; my $mailtrap_gid; my $check_mail_permissions_domain = ''; my $check_mail_permissions_sender = ''; my $check_mail_permissions_msgid = ''; my $check_mail_permissions_data = ''; my $check_mail_permissions_is_mailman = 0; my $enforce_mail_permissions_data = ''; my $primary_hostname; my %uid_cache = ( 0 => 'root', 47 => 'mailnull', 99 => 'nobody' ); my %user_cache = ( 'root' => 0, 'mailnull' => 47, 'nobody' => 99 ); my $reattempt_message = 'Message will be reattempted later'; my $sender_lookup; my $sender_lookup_method; # TEST VARIABLES my $check_mail_permissions_result; my %file_exists_cache; sub file_exists { return $file_exists_cache{ $_[0] } if exists $file_exists_cache{ $_[0] }; $file_exists_cache{ $_[0] } = -e $_[0] ? 1 : 0; return $file_exists_cache{ $_[0] }; } sub checkbx_autowhitelist { my $address = shift; my $phost = Exim::expand_string('$primary_hostname'); my $rp = Exim::expand_string('$received_protocol'); if ( $rp eq 'local' || $rp !~ /^e?smtps?a$/i || !$address || $address eq '' ) { return 'no'; } my ( $localpart, $domain ) = split( /\@/, $address ); if ( ( !$domain || $domain eq '' || $domain eq $phost ) ) { my $homedir = gethomedir($localpart); unless ( $homedir ne '' ) { return 'no'; } if ( -e $homedir . '/etc/.boxtrapperenable' && !-e $homedir . '/etc/.boxtrapperautowhitelistdisable' ) { return 'yes'; } else { return 'no'; } } else { my $owner = getdomainowner($domain); my $homedir = gethomedir($owner); unless ( $homedir ne '' ) { return 'no'; } my $passwd = "${homedir}/etc/${domain}/passwd"; my $addressexists = user_exists_in_db( $localpart, $passwd ); if ( $addressexists && ( -e $homedir . "/etc/${domain}/${localpart}/.boxtrapperenable" && !-e $homedir . "/etc/${domain}/${localpart}/.boxtrapperautowhitelistdisable" ) ) { return 'yes'; } else { return 'no'; } } } sub getemailuser { my ( $address, $received_protocol, $sender_ident ) = @_; my $primary_hostname = Exim::expand_string('$primary_hostname'); my ( $local_part, $domain ) = split( m/[\@\+\%\:]/, ( $address || ( $received_protocol && $received_protocol eq 'local' ? $sender_ident : '' ) ) ); if ( !$domain || $domain eq '' || $domain eq $primary_hostname ) { return $local_part; } else { my $user = getdomainowner($domain); if ($user) { return $user; } } return 'nobody'; } #DO NOT REMOVE THIS COMMENT AS IT TELLS CPANEL TO ENABLE SERVICE AUTH CHECKING #exim:serviceauth=1 # # Checkpass not used since auth is passed to dovecot SASL { no warnings 'redefine'; sub checkuserpass { 0; } sub checkpass { 0; } } sub checkspam { # This is an old code block that should never be reached unless there is a serious # problem installing their exim configuration Exim::log_write("Something went very wrong during the exim configuration update. Please try reinstalling your exim configuration."); 1; } sub convert_address_directory_to_dovecot_lda_destination_username { my $local_part = Exim::expand_string('$local_part'); my $domain = Exim::expand_string('$domain'); $primary_hostname ||= Exim::expand_string('$primary_hostname'); my $address_file = Exim::expand_string('$address_file'); if ( $address_file !~ m{mail/\Q$domain\E} ) { return ( getpwuid($>) )[0]; } else { return $local_part . '@' . $domain; } } sub convert_address_directory_to_dovecot_lda_mailbox { my $address_file = Exim::expand_string('$address_file'); my ($mailbox) = $address_file =~ m{/\.([^\/]+)}; if ($mailbox) { return "INBOX.$mailbox"; } return 'INBOX'; } sub call_cpwrap { my ( $function, @ARGS ) = @_; my @JSON_ENCODED_ARGS = map { aggressive_json_safe_encode($_) } @ARGS; my $data = join( ' ', @JSON_ENCODED_ARGS ); my $json_template = qq[{"function":"$function","namespace":"Cpanel","version":2,"action":"run","data":"$data","send_data_only":1,"module":"exim"}\r\n\r\n]; require Cpanel::Encoder::Exim; return eval { Exim::expand_string( '${readsocket{/usr/local/cpanel/var/cpwrapd.sock}{' . Cpanel::Encoder::Exim::unquoted_encode_string_literal($json_template) . '}{10s}}' ); }; } sub aggressive_json_safe_encode { my ($arg) = @_; $arg =~ tr/^a-zA-Z0-9!#\$\-=?^_{}~:.//cd; return $arg; } my $archived_at_domain_level = 0; my $archived_outgoing = 0; my $archived_mailman = 0; sub should_archive_incoming_domain_message { return ( $archived_at_domain_level = !_message_has_been_seen() ); } sub _message_has_been_seen { #ARCHIVE ONLY IF # #$parent_domain = "" # #OR # #$parent_domain != $domain # Delivery was not a result of an expansion my $parent_domain = Exim::expand_string('$parent_domain'); if ( !length $parent_domain ) { return 0; } # Delivery was the result of an expansion / alias. Since its a diffrent domain we don't # know if it was archived so we need to archive if enabled my $domain = Exim::expand_string('$domain'); if ( $domain ne $parent_domain ) { return 0; } my $parent_local_part = Exim::expand_string('$parent_local_part'); my $local_part = Exim::expand_string('$local_part'); # case 60975: If any deliveries happened, parent_domain and parent_local_part # will get set to match domain and local_part. Since we need to # still archive outgoing if it to our same domain or a local # user we need to accept when they all match if ( $parent_domain eq $domain && $local_part && $parent_local_part ) { return 0; } # parent_local_part ne local_part and # parent_domain == domain so it already got archived if we have it on return 1; } sub archive_headers { my ($router) = @_; if ( $router eq 'archive_incoming_email_domain_method' ) { return "X-Archive-Type: incoming\nX-Archive-Recipient: " . Exim::expand_string('$local_part') . '@' . Exim::expand_string('$domain'); } elsif ( $router eq 'archive_incoming_email_local_user_method' ) { return "X-Archive-Type: incoming\nX-Archive-Recipient: " . Exim::expand_string('$local_part'); } elsif ( $router eq 'archive_outgoing_email' ) { return "X-Archive-Type: " . $outgoing_sender_archive_directory . "\nX-Archive-Sender: $outgoing_sender"; } } sub should_archive_incoming_localuser_message { # case 60999: Do not archive a message at the localuser level # if we have already archived it at the domain level (avoid two copies) return 0 if $archived_at_domain_level; my $local_part = Exim::expand_string('$local_part'); my $incoming_domain = getusersdomain($local_part); if ($incoming_domain) { my $home = gethomedir($local_part); if ( file_exists("$home/etc/$incoming_domain/archive/incoming") ) { return 1; } } return 0; } sub get_incoming_domain { return getusersdomain( Exim::expand_string('$local_part') ); } sub should_archive_outgoing_message { return 0 if _message_has_been_seen(); return determine_sender_and_check_if_archive_needed(); } sub determine_sender_and_check_if_archive_needed { my $uid = int( Exim::expand_string('$originator_uid') ); my $gid = int( Exim::expand_string('$originator_gid') ); # outgoing_sender_domain is the domain of the actual sender # outgoing_sender_counted_domain is the domain we actually count the message against # Currently these are always the same except domain may be # rewritten if we are coming from a mailman list in order # to count against the owner of the list instead of the mailman # user assuming /var/cpanel/email_send_limits/count_mailman exists ( $outgoing_sender, $outgoing_sender_domain, $outgoing_sender_counted_domain, $outgoing_sender_is_mailman ) = get_message_sender( $uid, $gid ); if ( $outgoing_sender_domain && $outgoing_sender_domain ne '-system-' ) { $outgoing_sender_sysuser = getdomainowner($outgoing_sender_domain); my $home = gethomedir($outgoing_sender_sysuser); if ( $outgoing_sender_is_mailman && file_exists("$home/etc/$outgoing_sender_domain/archive/mailman") ) { $outgoing_sender_archive_directory = 'mailman'; return 0 if $archived_mailman; # already archived return ( $archived_mailman = 1 ); } elsif ( file_exists("$home/etc/$outgoing_sender_domain/archive/outgoing") ) { $outgoing_sender_archive_directory = 'outgoing'; return 0 if $archived_outgoing; # already archived return ( $archived_outgoing = 1 ); } } return 0; } sub pack_archive_address_data { my ($router) = @_; return join( ' ', 'router=' . Cpanel::Encoder::Exim::encode_string_literal($router), 'sender=' . Cpanel::Encoder::Exim::encode_string_literal($outgoing_sender), 'sender_domain=' . Cpanel::Encoder::Exim::encode_string_literal($outgoing_sender_domain), 'sender_sysuser=' . Cpanel::Encoder::Exim::encode_string_literal($outgoing_sender_sysuser), 'sender_archive_directory=' . Cpanel::Encoder::Exim::encode_string_literal($outgoing_sender_archive_directory) ); } sub get_outgoing_sender { return ( $outgoing_sender // Exim::expand_string('${extract{sender}{$address_data}}')); } sub get_outgoing_sender_domain { return ( $outgoing_sender_domain // Exim::expand_string('${extract{sender_domain}{$address_data}}')); } sub get_outgoing_sender_sysuser { return ( $outgoing_sender_sysuser // Exim::expand_string('${extract{sender_sysuser}{$address_data}}')); } sub get_outgoing_archive_directory { return ( $outgoing_sender_archive_directory // Exim::expand_string('${extract{sender_archive_directory}{$address_data}}')); } sub YYYYMMDDGMT { my ( $sec, $min, $hour, $mday, $mon, $year ) = gmtime( $_[0] || time() ); return sprintf( '%04d-%02d-%02d', $year + 1900, $mon + 1, $mday ); } our $DEFAULT_EMAIL_SEND_LIMITS_DEFER_CUTOFF_PERCENTAGE = 125; sub getmaxemailsperhour { my $domain = shift; return 0 if $domain eq '-system-'; $domain =~ s/\///g; #jic my $maxemails = 0; # Defaults to "unlimited" my $master_email_send_limits_mtime = ( stat('/etc/email_send_limits') )[9]; my $max_fh; if ( open( $max_fh, '<', '/var/cpanel/email_send_limits/cache/' . $domain ) && ( stat($max_fh) )[9] > $master_email_send_limits_mtime ) { # This is the user's main domain. All user's domains are aggregated here $maxemails = readline $max_fh; close $max_fh; return 0 if !$maxemails || $maxemails eq 'unlimited'; return ( $maxemails ? int($maxemails) : 0 ); } my $search_regex = qr/^\Q$domain\E:/; my $search_wildcard_regex = qr/^\Q*\E:/; _check_cache_dir(); my $old_umask = umask(); umask(0027); #format DOMAIN: MAX_EMAIL_PER_HOUR,MAX_DEFER_FAIL_PERCENTAGE,MIN_DEFER_FAIL_TO_TRIGGER_PROTECTION if ( open( my $max_fh, '>', '/var/cpanel/email_send_limits/cache/.' . $domain ) ) { umask($old_umask); if ( open( my $email_limits_fh, '<', '/etc/email_send_limits' ) ) { while ( readline($email_limits_fh) ) { if ( $_ =~ $search_regex ) { $maxemails = ( split( /\,/, ( split( /:\s+/, $_ ) )[1] ) )[0]; last if $maxemails || $maxemails eq '0'; # case 51568: if there is no value we use the wildcard } elsif ( $_ =~ $search_wildcard_regex ) { $maxemails = ( split( /\,/, ( split( /:\s+/, $_ ) )[1] ) )[0]; last; } } } chomp $maxemails; print {$max_fh} $maxemails; close($max_fh); rename( '/var/cpanel/email_send_limits/cache/.' . $domain, '/var/cpanel/email_send_limits/cache/' . $domain ); #rename is atomic and will overwrite the file return int $maxemails; # case 51568: must transform 'unlimited' to 0 } else { umask($old_umask); } return 0; } sub increment_max_emails_per_hour { my ( $domain, $time, $msgid ) = @_; $domain =~ s/\///g; #jic _check_tracker_dir($domain); $time ||= time(); Exim::log_write( "SMTP connection outbound $time $msgid $domain " . Exim::expand_string('$local_part') . '@' . Exim::expand_string('$domain') ); if ( open( my $emailt_fh, '>>', "/var/cpanel/email_send_limits/track/$domain/" . join( '.', ( gmtime($time) )[ 2, 3, 4, 5 ] ) ) ) { print {$emailt_fh} '1'; close($emailt_fh); } # !DEBUG! # if ( open( my $emailt_fh, '>>', "/var/cpanel/email_send_limits/track/$domain/msgids_" . join( '.', ( gmtime( $time ) )[ 2, 3, 4, 5 ] ) ) ) { # # print {$emailt_fh} $msgid . "\n"; # close($emailt_fh); # } } sub _check_cache_dir { mkdir( '/var/cpanel/email_send_limits/cache', 0750 ) if !-e '/var/cpanel/email_send_limits/cache'; } sub _check_tracker_dir { my $domain = shift; $domain =~ s/\///g; #jic if ( !-e '/var/cpanel/email_send_limits/track/' . $domain ) { mkdir( '/var/cpanel/email_send_limits', 0751 ); mkdir( '/var/cpanel/email_send_limits/track', 0750 ); mkdir( '/var/cpanel/email_send_limits/track/' . $domain, 0750 ); } } sub get_current_emails_per_hour { ( ( stat( "/var/cpanel/email_send_limits/track/$_[0]/" . join( '.', ( gmtime( $_[1] || time() ) )[ 2, 3, 4, 5 ] ) ) )[7] || 0 ); } sub get_current_emails_per_day { my $domain = shift; $domain =~ s/\///g; #jic return 0 if ( !-e '/var/cpanel/email_send_limits/track/' . $domain ); my $total_size = 0; if ( opendir( my $domain_track_fh, '/var/cpanel/email_send_limits/track/' . $domain ) ) { while ( my $domaintime = readdir($domain_track_fh) ) { next if ( $domaintime =~ /^\.\.?$/ ); my $tracker_file_size = ( stat("/var/cpanel/email_send_limits/track/$domain/$domaintime") )[7]; $total_size += $tracker_file_size; } } return $total_size; } sub reached_max_emails_per_hour { my $domain = shift; $domain =~ s/\///g; #jic my $max_allowed = int( shift || 0 ); my $time = shift || time(); if ($max_allowed) { # AKA number_of_emails_sent >= $max_allowed if ( get_current_emails_per_hour( $domain, $time ) >= $max_allowed ) { return 1; } else { return 0; } } return 0; } # # This converse function for reference only # #sub set_email_send_limits_defer_cutoff { # my $percentage = int shift ; # # # The value is the size of the file so we can avoid the open/close overhead (just a stat) # if ( open(my $cut_off_percentage_fh,'>','/var/cpanel/email_send_limits/defer_cutoff') ) { # print {$cut_off_percentage_fh} 'x' x $percentage; # return 1; # } # # return 0; # } sub get_email_send_limits_defer_cutoff { # The value is the size of the file so we can avoid the open/close overhead (just a stat) my $cut_off_percentage = ( stat('/var/cpanel/email_send_limits/defer_cutoff') )[7]; if ( !defined $cut_off_percentage ) { $cut_off_percentage = $DEFAULT_EMAIL_SEND_LIMITS_DEFER_CUTOFF_PERCENTAGE; } return $cut_off_percentage; } # # This converse function for reference only # # sub set_email_daily_limit_notify { # my $limit = int shift ; # if ( $limit == 0 ) { # unlink '/var/cpanel/email_send_limits/daily_limit_notify'; # return 1; # } # # The value is the size of the file so we can avoid the open/close overhead (just a stat) # if ( open(my $daily_limit_fh,'>','/var/cpanel/email_send_limits/daily_limit_notify') ) { # print {$daily_limit_fh} 'x' x $limit; # return 1; # } # return 0; # } sub get_email_daily_limit_notify { # The value is the size of the file so we can avoid the open/close overhead (just a stat) my $limit = ( stat('/var/cpanel/email_send_limits/daily_limit_notify') )[7]; if ( !defined $limit ) { $limit = 0; } return $limit; } sub create_daily_notify_touchfile { my $domain = shift; $domain =~ s/\///g; #jic mkdir( '/var/cpanel/email_send_limits/daily_notify', 0750 ) if !-e '/var/cpanel/email_send_limits/daily_notify'; if ( open( my $daily_limit_fh, '>', '/var/cpanel/email_send_limits/daily_notify/' . $domain ) ) { close $daily_limit_fh; } return undef; } BEGIN { unshift @INC, '/usr/local/cpanel'; } #DO NOT USE lib here # use Cpanel::Encoder::Exim (); -- no loaded with require or preload sub gethomedir { my $user = shift; require Cpanel::Encoder::Exim; return Exim::expand_string( '${extract{5}{:}{${lookup passwd{' . Cpanel::Encoder::Exim::unquoted_encode_string_literal($user) . '}{$value}}}}' ) || ''; } sub getuid { my $user = shift; require Cpanel::Encoder::Exim; my $uid = Exim::expand_string( '${extract{2}{:}{${lookup passwd{' . Cpanel::Encoder::Exim::unquoted_encode_string_literal($user) . '}{$value}}}}' ); return defined $uid ? $uid : ''; } sub getdomainowner { my $domain = shift; require Cpanel::Encoder::Exim; substr($domain,0,4,'') if index($domain,'www.') == 0; return Exim::expand_string( '${lookup{' . Cpanel::Encoder::Exim::unquoted_encode_string_literal($domain) . '}lsearch{/etc/userdomains}{$value}}' ) || ''; } my %domain_to_user_cache; # This must be cached because we call getusersdomain as root in the archive_incoming_email_local_user_method router # and then we need to read the user out of the memory cache in archiver_incoming_local_user_method since # we no longer have access to read /etc/domainusers at that point. Note, we need to be able to cache multiple # users in case they send a message to multiple system users sub getusersdomain { return '' if !$_[0] || $_[0] eq 'root' || $_[0] =~ tr{/}{} || !-e "/var/cpanel/users/$_[0]"; return ( $domain_to_user_cache{ $_[0] } || ( $domain_to_user_cache{ $_[0] } = lookup_key_in_file( '/etc/domainusers', $_[0] ) ) ); } sub lookup_key_in_file { my ( $file, $key ) = @_; require Cpanel::Encoder::Exim; return Exim::expand_string( '${lookup{' . Cpanel::Encoder::Exim::unquoted_encode_string_literal($key) . '}lsearch{' . $file . '}{$value}}' ) || ''; } sub isdemo { my $user = shift; return if ( !$user ); return 0 if $user eq '0' || $user eq '8' || $user eq 'mail' || $user eq 'mailnull' || $user eq 'root'; if ( $user =~ /^\d+$/ ) { return user_exists_in_db( $user, '/etc/demouids' ); } return user_exists_in_db( $user, '/etc/demousers' ); } sub user_exists_in_db { my ( $user, $db ) = @_; # If the user is empty, '0' or only whitespace # we should return 0 as $lookup will always return # 1 even if it does not exist return 0 if !$user || $user !~ tr{ \t}{}c; require Cpanel::Encoder::Exim; return Exim::expand_string( '${lookup{' . Cpanel::Encoder::Exim::unquoted_encode_string_literal($user) . '}lsearch{' . $db . '}{1}{0}}' ) || '0'; } my %sender_recent_authed_mail_ips_address_cache; my $get_recent_authed_mail_ips_lookup_method; sub get_recent_authed_mail_ips_text_entry { my ( $sender, $domain ) = get_recent_authed_mail_ips_entry(@_); return join( '|', ( $sender || '' ), $domain ); } sub popbeforesmtpwarn { if ( my @possible_users = _get_possible_users_from_recent_authed_mail_ips_users() ) { return ( "X-PopBeforeSMTPSenders: " . join( ",", @possible_users ) ); } return ''; } sub get_recent_authed_mail_ips_entry { my $log = shift; # SENDING OVER POP B4 SMTP or NOAUTH # case 43151, case 43150 $get_recent_authed_mail_ips_lookup_method = ''; my $sender_host_address = Exim::expand_string('$sender_host_address'); # Exim::log_write("!DEBUG! get_recent_authed_mail_ips_entry sender_host_address=[$sender_host_address] log=[$log]"); my ( $sender, $domain ); if ( exists $sender_recent_authed_mail_ips_address_cache{$sender_host_address} ) { # Exim::log_write("!DEBUG! get_recent_authed_mail_ips_entry sender_host_address=[$sender_host_address] USING CACHE"); ( $sender, $domain, $get_recent_authed_mail_ips_lookup_method ) = @{ $sender_recent_authed_mail_ips_address_cache{$sender_host_address} }; $get_recent_authed_mail_ips_lookup_method = "cached: " . $get_recent_authed_mail_ips_lookup_method; $log = 0; } else { my $recent_authed_mail_ips_users_is_up_to_date = ( stat('/etc/recent_authed_mail_ips_users') )[9] + 7200 > time() ? 1 : 0; my $sender_address_domain; # Exim::log_write("!DEBUG! get_recent_authed_mail_ips_entry sender_host_address=[$sender_host_address] recent_authed_mail_ips_users_is_up_to_date= $recent_authed_mail_ips_users_is_up_to_date"); # If we have a recent_authed_mail_ips_users file that is up to date, we can verify the ip matches if ($recent_authed_mail_ips_users_is_up_to_date) { # This is what the user has claimed as the sender my $sender_address = Exim::expand_string('$sender_address'); my $from_h_domain = Exim::expand_string('${domain:$h_from:}'); my $from_h_localpart = Exim::expand_string('${local_part:$h_from:}'); my $from_h = "$from_h_localpart\@$from_h_domain"; # First we try to find the address in the recent_authed_mail_ips_users file (with a cached exim lookup) if ( my @possible_users = _get_possible_users_from_recent_authed_mail_ips_users() ) { if ( grep { tr/@// ? $from_h eq $_ : $from_h eq $_ . '@' . $primary_hostname } @possible_users ) { $sender = $from_h; $domain = getdomainfromaddress($from_h); $get_recent_authed_mail_ips_lookup_method = "full match of from_h in recent_authed_mail_ips_users"; } elsif ( grep { tr/@// ? $sender_address eq $_ : $sender_address eq $_ . '@' . $primary_hostname } @possible_users ) { $sender = $sender_address; $domain = getdomainfromaddress($sender_address); $get_recent_authed_mail_ips_lookup_method = "full match of sender_address in recent_authed_mail_ips_users"; } elsif ( ( $sender_address_domain = ( split( m/\@/, $sender_address ) )[1] ) && grep( m/\@\Q$sender_address_domain\E$/, @possible_users ) ) { $domain = $sender_address_domain; $sender = '-unknown-@' . $domain; $get_recent_authed_mail_ips_lookup_method = "match of sender_address_domain in recent_authed_mail_ips_users"; } elsif ( grep { tr/@// ? ( $from_h eq $_ ) : ( $from_h_localpart eq $_ && ( !length $from_h_domain || $from_h_domain eq $primary_hostname ) ) } @possible_users ) { $sender = $from_h; $domain = $from_h_domain; $get_recent_authed_mail_ips_lookup_method = "full match of from_h in recent_authed_mail_ips_users"; } elsif ( grep( m/\@\Q$from_h_domain\E$/, @possible_users ) ) { $domain = $from_h_domain; $sender = '-unknown-@' . $from_h_domain; $get_recent_authed_mail_ips_lookup_method = "match of from_h_domain in recent_authed_mail_ips_users"; } elsif ( $possible_users[0] && $possible_users[0] eq '-alwaysrelay-' ) { if ($from_h_domain) { Exim::log_write("$sender_host_address in /etc/alwaysrelay trusting from_h_domain of: $from_h_domain and from_h_localpart: $from_h_localpart"); $domain = $from_h_domain; $sender = $from_h; $get_recent_authed_mail_ips_lookup_method = "in alwaysrelay trusted from_h"; } else { Exim::log_write("$sender_host_address in /etc/alwaysrelay trusting sender_address_domain of: $sender_address_domain"); $domain = $sender_address_domain; $sender = $sender_address; $get_recent_authed_mail_ips_lookup_method = "in alwaysrelay trusted sender_address"; } } else { # If none of them matched, we have to assume they authenticated in some we so we go with the first one $domain = getdomainfromaddress( $possible_users[0] ); $sender = $possible_users[0]; $get_recent_authed_mail_ips_lookup_method = "in recent_authed_mail_ips_users using first address"; } if ( $sender =~ m/^\*/ ) { $sender =~ s/^\*/-unknown-/; } $sender_recent_authed_mail_ips_address_cache{$sender_host_address} = [ $sender, $domain, $get_recent_authed_mail_ips_lookup_method ]; } } # we need to check alwaysrelay since we don't require recentauthedmailiptracker to be enabled if ( !$domain && -e '/etc/alwaysrelay' ) { my $alwaysrelay_result = Exim::expand_string('${lookup{$sender_host_address}iplsearch{/etc/alwaysrelay}{$sender_host_address $value}}'); if ($alwaysrelay_result) { my ( $alwaysrelay_ip, $alwaysrelay_user ) = split( /\s+/, $alwaysrelay_result ); if ($alwaysrelay_user) { $domain = getdomainfromaddress($alwaysrelay_user); $sender = $alwaysrelay_user; $get_recent_authed_mail_ips_lookup_method = "full match in alwaysrelay with recentauthedmailiptracker disabled"; Exim::log_write("$sender_host_address in /etc/alwaysrelay using domain $domain from lookup of $alwaysrelay_user"); } if ( !$domain ) { $domain = $sender_address_domain = ( split( /\@/, Exim::expand_string('$sender_address') ) )[1]; $sender = "-unknown-\@$domain"; $get_recent_authed_mail_ips_lookup_method = "in alwaysrelay with recentauthedmailiptracker disabled"; Exim::log_write("$sender_host_address in /etc/alwaysrelay trusting sender_address_domain of: $sender_address_domain"); } } # no need to check /etc/alwaysrelay as they are automaticlly built into recent_authed_mail_ips_users } } if ($domain) { if ($log) { my $message_exim_id = Exim::expand_string('$message_exim_id'); my $sender_host_name = Exim::expand_string('${if match_ip{$sender_host_address}{+loopback}{localhost}{$sender_host_name}}'); my $sender_host_port = Exim::expand_string('$sender_host_port'); my $recent_authed_mail_ips_local_user = getdomainowner($domain); my $recent_authed_mail_ips_local_uid = user2uid($recent_authed_mail_ips_local_user); Exim::log_write("SMTP connection identification H=$sender_host_name A=$sender_host_address P=$sender_host_port U=$recent_authed_mail_ips_local_user ID=$recent_authed_mail_ips_local_uid S=$sender B=get_recent_authed_mail_ips_entry"); } return ( $sender, $domain, $get_recent_authed_mail_ips_lookup_method ); } return ( '', '', '' ); } sub _get_possible_users_from_recent_authed_mail_ips_users { my $recent_authed_mail_ips_users_result = Exim::expand_string('${lookup{$sender_host_address}lsearch{/etc/recent_authed_mail_ips_users}{$value}}'); return map { s/\/.*$//g if tr/\///; tr/+%:/@/; $_; } split( m/\s*\,\s*/, $recent_authed_mail_ips_users_result ); } my $local_connection_uid; my $local_connection_user; my %sender_host_address_cache; sub get_identified_local_connection_uid { $local_connection_uid; } sub get_identified_local_connection_user { $local_connection_user; } sub identify_local_connection { # passes but not for production # use strict; # On Linux we can identify users by reading /proc/net/tcp* # Since this requires access kernel memory on bsd and we don't have a way # do that under exim users MUST authenticate to send messages from localhost my ( $sender_host_address, $sender_host_port, $received_ip_address, $received_port, $log ) = @_; undef $local_connection_uid; undef $local_connection_user; my $uid; if ( exists $sender_host_address_cache{ $sender_host_address . '__' . $sender_host_port } ) { $uid = $sender_host_address_cache{ $sender_host_address . '__' . $sender_host_port }; $log = 0; } else { local @INC = ( '/usr/local/cpanel', @INC ) if !grep { '/usr/local/cpanel' } @INC; require Cpanel::Ident; $uid = Cpanel::Ident::identify_local_connection( $sender_host_address, $sender_host_port, $received_ip_address, $received_port ); if ( !defined $uid ) { $uid = identify_local_connection_wrapped( $sender_host_address, $sender_host_port, $received_ip_address, $received_port ); } } if ( defined $uid ) { $local_connection_uid = $uid; $sender_host_address_cache{ $sender_host_address . '__' . $sender_host_port } = $local_connection_uid; if ( $uid == -1 ) { Exim::log_write("Could not identify the local connection from $sender_host_address on port $sender_host_port. Please authenticate") if $log; return 0; } $local_connection_user = uid2user($uid); # Log this for tailwatchd Exim::log_write("SMTP connection identification H=localhost A=$sender_host_address P=$sender_host_port U=$local_connection_user ID=$local_connection_uid S=$local_connection_user B=identify_local_connection") if $log; return 1; } else { $sender_host_address_cache{ $sender_host_address . '__' . $sender_host_port } = undef; Exim::log_write("could not identify the local connection from $sender_host_address on port $sender_host_port. Please authenticate") if $log; return 0; } } sub identify_local_connection_wrapped { my ( $address, $port, $localaddress, $localport ) = @_; my $uidline = call_cpwrap( 'IDENTIFYLOCALCONNECTION', $address, $port, $localaddress, $localport ); chomp($uidline) if defined $uidline; my ( $uidkey, $uid ) = split( /:/, $uidline, 2 ); $uid = undef if $uid eq ''; Exim::log_write("/usr/local/cpanel/bin/eximwrap IDENTIFYLOCALCONNECTION $address $port $localaddress $localport failed to return the uid key.") if ( !defined $uidkey || $uidkey ne 'uid' ); return $uid; } my $headers_rewrite_notice = ''; my $new_from_header; use constant { _ENOENT => 2, _EEXIST => 17, _SENDER_SYSTEM => '-system-', }; sub spamd_is_available { require Cpanel::Services::Enabled::Spamd; return eval { Cpanel::Services::Enabled::Spamd::is_enabled() } // do { warn; 1; # this defaults to on for historical reasons }; } sub get_dkim_domain { my $msg_sender_domain = get_message_sender_domain(); if ($msg_sender_domain eq _SENDER_SYSTEM) { $msg_sender_domain = Exim::expand_string('$sender_address_domain'); } return $msg_sender_domain =~ tr<A-Z><a-z>r; } sub sender_domain_can_dkim_sign { require Cpanel::DKIM::ValidityCache; my $sender_domain = get_dkim_domain(); local $@; return eval { Cpanel::DKIM::ValidityCache->get($sender_domain) } // do { warn; q<>; }; } sub discover_sender_information { # If $sender_lookup_method and $check_mail_permissions_sender is already set # we have already discovered the sender if ( !$sender_lookup_method || !$check_mail_permissions_sender ) { my $uid = int( Exim::expand_string('$originator_uid') ); my $gid = int( Exim::expand_string('$originator_gid') ); #Exim::log_write("discover_sender_information calling get_message_sender"); my ( $sender, $real_domain, $domain, $is_mailman ) = get_message_sender( $uid, $gid, 1 ); $check_mail_permissions_sender = $sender if $sender; $check_mail_permissions_is_mailman = $is_mailman; } #Exim::log_write("discover_sender_information calling discover_sender_information"); $new_from_header = get_from_header_rewrite_target(); return 0; } sub get_headers_rewrite { return $new_from_header if $new_from_header; my ($from_h_sender) = _get_from_h_sender(); Exim::log_write("discover_sender_information failed to set the from header rewrite for $from_h_sender"); return $from_h_sender; } sub get_from_header_rewrite_target { $headers_rewrite_notice = ''; my ( $from_h_sender, $from_h_localpart, $from_h_domain ) = _get_from_h_sender(); if ( $sender_lookup_method && $check_mail_permissions_sender ) { my $actual_sender = _get_login_from_check_mail_permissions_sender($check_mail_permissions_sender); #Exim::log_write("!DEBUG! get_from_header_rewrite_target() actual_sender=[$actual_sender] from_h_sender=[$from_h_sender]"); my $qualified_actual_sender = _qualify_as_email_address($actual_sender); my ( $status, $statusmsg ); if ( $sender_lookup_method =~ m{^redirect/forwarder} ) { $headers_rewrite_notice = 'unmodified, forwarded message'; return $from_h_sender; } elsif ($check_mail_permissions_is_mailman) { $headers_rewrite_notice = 'unmodified, sender is mailman'; return $from_h_sender; } elsif ( $from_h_sender eq $actual_sender ) { $headers_rewrite_notice = 'unmodified, already matched'; return $from_h_sender; } else { if ( $actual_sender eq 'mailnull' ) { # handle Mailer-Daemon messages $headers_rewrite_notice = 'unmodified, actual sender is mailnull'; return $from_h_sender; } my $from_h_sender_domainowner = getdomainowner($from_h_domain); # Actual Sender is a system user. if ( $from_h_sender_domainowner && $from_h_sender_domainowner eq $actual_sender ) { $headers_rewrite_notice = 'unmodified, actual sender is system user that owns from domain in the from header'; return $from_h_sender; } elsif ( $from_h_sender eq $qualified_actual_sender ) { $headers_rewrite_notice = 'unmodified, actual sender is the system user'; return $from_h_sender; } elsif ( $actual_sender eq 'root' ) { $headers_rewrite_notice = 'unmodified, actual sender is root'; return $from_h_sender; } elsif ( $actual_sender eq 'mailman' ) { $headers_rewrite_notice = 'unmodified, actual sender is mailman'; return $from_h_sender; } elsif ( $actual_sender !~ tr/\@// && _is_trusted_user($actual_sender) ) { $headers_rewrite_notice = 'unmodified, actual sender is a trusted user'; return $from_h_sender; } elsif ( ( ( $status, $statusmsg ) = _has_valias_pointing_to_actual_sender( $from_h_sender, $actual_sender ) )[0] ) { if ( $statusmsg eq 'valias_exact_match' ) { $headers_rewrite_notice = 'unmodified, there is a forwarder that points to the actual sender.'; } elsif ( $statusmsg eq 'valias_domainowner_match' ) { $headers_rewrite_notice = 'unmodified, there is a forwarder that points to a user owned by actual sender.'; } elsif ( $statusmsg eq 'vdomainaliases_match' ) { $headers_rewrite_notice = 'unmodified, there is a domain forwarder that maps to the actual sender.'; } return $from_h_sender; } else { if ( $actual_sender !~ tr/\@// ) { $headers_rewrite_notice = 'rewritten was: [' . $from_h_sender . '], actual sender is not the same system user'; } else { $headers_rewrite_notice = 'rewritten was: [' . $from_h_sender . '], actual sender does not match'; } Exim::log_write("From: header ($headers_rewrite_notice) original=[$from_h_sender] actual_sender=[$qualified_actual_sender]"); return $qualified_actual_sender; } } } # We have no sender set so we leave it unmodified # AKA unable to determine sender would get here $headers_rewrite_notice = 'unmodified, no actual sender determined from check mail permissions'; return $from_h_sender; } sub get_headers_rewritten_notice { if ($headers_rewrite_notice) { return "X-From-Rewrite: $headers_rewrite_notice"; } return ''; } # # This converts an unqualified address which is just a system # account IE local_part. Into local_part@primary_hostname. # # If the address is already qualified ie has @, it returns returns the # address. # sub _qualify_as_email_address { my ($address) = @_; return $address if $address =~ tr/@//; $primary_hostname ||= Exim::expand_string('$primary_hostname'); return $address . '@' . $primary_hostname; } # # Convert the $check_mail_permissions_sender variable # into the real login that the user has authenticated as # in most cases this is already their email address, however it may # be USER@PRIMARY_HOSTNAME, in which case we want to strip PRIMARY_HOSTNAME # sub _get_login_from_check_mail_permissions_sender { my ($sender) = @_; $primary_hostname ||= Exim::expand_string('$primary_hostname'); $sender =~ s/\@\Q$primary_hostname\E$//; return $sender; } # _has_valias_pointing_to_target lets us know if there # if a forwarder for the address pointing at the target. # # For example ORIGIN bob@cpanel.net # might point to a user account DEST 'bob' # sub _has_valias_pointing_to_actual_sender { my ( $origin, $actual_sender ) = @_; #Exim::log_write("!DEBUG! _has_valias_pointing_to_actual_sender() actual_sender=[$actual_sender] origin=[$origin]"); my $qualified_origin = _qualify_as_email_address($origin); my $qualified_actual_sender = _qualify_as_email_address($actual_sender); my ( $origin_local_part, $origin_domain ) = split( m{@}, $qualified_origin, 2 ); my ( $actual_sender_local_part, $actual_sender_domain ) = split( m{@}, $qualified_actual_sender, 2 ); my $actual_sender_domainowner; require Cpanel::Encoder::Exim; return ( 0, 'invalid_origin_domain' ) if $origin_domain =~ m{/}; if ( file_exists("$VALIASES_DIR/$origin_domain") ) { if ( my $valiases_alias_line = Exim::expand_string( '${lookup{' . Cpanel::Encoder::Exim::unquoted_encode_string_literal($origin) . '}lsearch*{' . $VALIASES_DIR . '/' . $origin_domain . '}{$value}}' ) ) { if ( my @forwarders = _get_forwarders_from_string($valiases_alias_line) ) { foreach my $forwarder_destination (@forwarders) { # # Handle exact matches # IE bob@cpanel.net is forwarded to the actual sender # if ( _qualify_as_email_address($forwarder_destination) eq $qualified_actual_sender ) { return ( 1, 'valias_exact_match' ); } # $VALIASES_DIR/dog.com: nick@dog.org: me@samsdomain.org # I send email From: nick@dog.org and I am authenticated as 'sam' it should likely be allowed if ( $actual_sender !~ tr/\@// && $forwarder_destination =~ tr/\@// ) { my ( $forwarder_destination_local_part, $forwarder_destination_domain ) = split( m{@}, $forwarder_destination, 2 ); my $forwarder_destination_domainowner = getdomainowner($forwarder_destination_domain); if ( $actual_sender eq $forwarder_destination_domainowner ) { return ( 1, 'valias_domainowner_match' ); } } } } } } if ( file_exists("$VDOMAINALIASES_DIR/$origin_domain") ) { if ( my $vdomainaliases_alias_line = Exim::expand_string( '${lookup{' . Cpanel::Encoder::Exim::unquoted_encode_string_literal($origin_domain) . '}lsearch{' . $VDOMAINALIASES_DIR . '/' . $origin_domain . '}{$value}}' ) ) { my $vdomainaliases_domain = _ws_trim($vdomainaliases_alias_line); if ( ( $origin_local_part . '@' . $vdomainaliases_domain ) eq $qualified_actual_sender ) { return ( 1, 'vdomainaliases_match' ); } } } return ( 0, 'no_match' ); } sub _is_trusted_user { my ($user) = @_; return 0 if !file_exists('/etc/trusted_mail_users'); local $/; open my $trusted_mail_users_fh, '<', '/etc/trusted_mail_users' or return 0; my @trusted_mail_users = split( qq{\n}, <$trusted_mail_users_fh> ); close $trusted_mail_users_fh; return scalar grep { $_ eq $user } @trusted_mail_users; } # # From Cpanel::StringFunc::Trim # sub _ws_trim { my ($this) = @_; my $fix = ref $this eq 'SCALAR' ? $this : \$this; ${$fix} =~ s/^\s+//; ${$fix} =~ s/\s+$//; return ${$fix}; } # # From Cpanel::API::Email # sub _get_forwarders_from_string { my ($forwarder_csv) = @_; # to leave \, as \, uncomment this: # $forwarder_csv =~ s{\\,}{\\\\,}g; my @forwarders = $forwarder_csv =~ /^[\s"]*\:(fail|defer|blackhole|include)\:/ ? ($forwarder_csv) : split( /(?<![\\]),/, $forwarder_csv ); my @parsed_forwarders; for my $forward (@forwarders) { $forward = _ws_trim($forward); next if ( $forward =~ m{^"} ); push @parsed_forwarders, $forward; } return wantarray ? @parsed_forwarders : \@parsed_forwarders; } sub check_mail_permissions_results { return $check_mail_permissions_data; } sub enforce_mail_permissions_results { $enforce_mail_permissions_data; } sub uid2user { my $uid = shift; return exists $uid_cache{$uid} ? $uid_cache{$uid} : ( $uid_cache{$uid} = ( getpwuid($uid) )[0] ); } sub user2uid { my $user = shift; return exists $user_cache{$user} ? $user_cache{$user} : ( $user_cache{$user} = getuid($user) ); } sub get_sender_from_uid { my $uid = int( Exim::expand_string('$originator_uid') ); my $user = uid2user($uid); return getdomainfromaddress($user); } sub mailtrapheaders { $primary_hostname ||= Exim::expand_string('$primary_hostname'); my $original_domain = Exim::expand_string('$original_domain'); my $sender_address_domain = Exim::expand_string('$sender_address_domain'); my $originator_uid = Exim::expand_string('$originator_uid'); my $originator_gid = Exim::expand_string('$originator_gid'); my $caller_uid = Exim::expand_string('$caller_uid'); my $caller_gid = Exim::expand_string('$caller_gid'); my $headers = "X-AntiAbuse: This header was added to track abuse, please include it with any abuse report\n" . "X-AntiAbuse: Primary Hostname - $primary_hostname\n" . "X-AntiAbuse: Original Domain - $original_domain\n" . "X-AntiAbuse: Originator/Caller UID/GID - [$originator_uid $originator_gid] / [$caller_uid $caller_gid]\n" . "X-AntiAbuse: Sender Address Domain - $sender_address_domain\n" . check_mail_permissions_headers() . "\n"; if ( file_exists('/etc/eximmailtrap') ) { my $xsource = $ENV{'X-SOURCE'}; my $xsourceargs = $ENV{'X-SOURCE-ARGS'}; my $xsourcedir = maskdir( $ENV{'X-SOURCE-DIR'} ); $headers .= "X-Source: ${xsource}\n" . "X-Source-Args: ${xsourceargs}\n" . "X-Source-Dir: ${xsourcedir}"; } return ($headers); } sub getdomainfromaddress { my $address = shift; $address =~ s/\/.*$//g if $address =~ tr/\///; # remove /spam if ( $address =~ tr/@+%:// ) { unless ( $address =~ tr/@// ) { # This matches exactly how authentication occurs $address =~ s/[+:%]/@/; } $primary_hostname ||= Exim::expand_string('$primary_hostname'); if ( $address =~ m/[@]\Q$primary_hostname\E$/ ) { return getusersdomain( ( split( m/[@]/, $address, 2 ) )[0] ) || _SENDER_SYSTEM; #from MailAuth.pm } else { return ( split( m/[@]/, $address, 2 ) )[1]; #from MailAuth.pm } } else { return getusersdomain($address) || _SENDER_SYSTEM; } } sub get_message_sender_domain { my ( $uid, $gid, $log ) = @_; $uid = int( Exim::expand_string('$originator_uid') ) if !defined $uid; $gid = int( Exim::expand_string('$originator_gid') ) if !defined $gid; return ( ( get_message_sender( $uid, $gid, $log ) )[1] ) || ''; } sub get_sender_lookup_method { return $sender_lookup_method || 'none'; } sub get_sender_lookup { return $sender_lookup || ''; } sub check_mail_permissions_headers { return "X-Get-Message-Sender-Via: " . ( $primary_hostname ||= Exim::expand_string('$primary_hostname') ) . ": " . get_sender_lookup_method() . "\n" . "X-Authenticated-Sender: " . ( $primary_hostname ||= Exim::expand_string('$primary_hostname') ) . ": " . get_sender_lookup(); } # This must match the logic extactly for Cpanel::TailWatch::EximStats ($direction eq '<=') sub get_message_sender { #passes but not for production #use strict; my ( $uid, $gid, $log ) = @_; my ( $authenticated_local_user, $authenticated_id, $recent_authed_mail_ips_text_entry, $domain, $counted_domain, $sender, $is_mailman, $username ); $sender_lookup_method = ''; my ( $acl_c_vhost_owner, $acl_c_vhost_owner_url ) = split( m{:}, Exim::expand_string('$acl_c_vhost_owner') || '', 2 ); my $message_exim_id = Exim::expand_string('$message_exim_id'); # SMTP AUTH if ( $authenticated_id = Exim::expand_string('$authenticated_id') ) { $authenticated_id =~ s/[\r\n\f]//g; if ( $authenticated_id eq 'nobody' ) { if ($acl_c_vhost_owner) { $authenticated_id = uid2user($acl_c_vhost_owner); } $sender_lookup_method = 'uid via acl_c_vhost_owner from authenticated_id: ' . $authenticated_id . ' from ' . $acl_c_vhost_owner_url; } else { $sender_lookup_method = 'authenticated_id: ' . $authenticated_id; } $sender = $authenticated_id; $domain = getdomainfromaddress($authenticated_id); # If the sender owns the domain they are sending # from we can trust it if ( length $sender && $sender !~ tr/\@// ) { ( $sender, $domain, $sender_lookup_method ) = resolve_authenticated_sender( $sender, $domain, $sender_lookup_method ); } #Exim::log_write("!DEBUG! get_message_sender() got domain $domain from authenticated_id ($authenticated_id)"); } # FROM A CONNECTION TO LOCALHOST (linux only) elsif ( $authenticated_local_user = Exim::expand_string('${if match_ip{$sender_host_address}{+loopback}{$acl_c_authenticated_local_user}{}}') ) { my $authenticated_local_uid = user2uid($authenticated_local_user); my $sender_host_address = Exim::expand_string('$sender_host_address'); my $sender_host_name = Exim::expand_string('${if match_ip{$sender_host_address}{+loopback}{localhost}{$sender_host_name}}'); my $sender_host_port = Exim::expand_string('$sender_host_port'); $domain = getusersdomain($authenticated_local_user) || _SENDER_SYSTEM; $sender = $authenticated_local_user; $sender_lookup_method = 'acl_c_authenticated_local_user: ' . $authenticated_local_user; if ($log) { Exim::log_write("SMTP connection identification H=$sender_host_name A=$sender_host_address P=$sender_host_port M=$message_exim_id U=$authenticated_local_user ID=$authenticated_local_uid S=$sender B=authenticated_local_user"); } #replay for tailwatchd #Exim::log_write("!DEBUG! get_message_sender() got domain $domain from acl_c_authenticated_local_user"); } # RELAY HOSTS elsif ( $recent_authed_mail_ips_text_entry = Exim::expand_string('$acl_c_recent_authed_mail_ips_text_entry') ) { #FIXME: need to get sender ( $sender, $domain ) = split( /\|/, $recent_authed_mail_ips_text_entry ); my $sender_host_address = Exim::expand_string('$sender_host_address'); my $sender_host_name = Exim::expand_string('${if match_ip{$sender_host_address}{+loopback}{localhost}{$sender_host_name}}'); my $sender_host_port = Exim::expand_string('$sender_host_port'); my $recent_authed_mail_ips_local_user = getdomainowner($domain); my $recent_authed_mail_ips_local_uid = user2uid($recent_authed_mail_ips_local_user); $sender_lookup_method = 'acl_c_recent_authed_mail_ips_text_entry: ' . $recent_authed_mail_ips_text_entry; if ($log) { Exim::log_write("SMTP connection identification H=$sender_host_name A=$sender_host_address P=$sender_host_port M=$message_exim_id U=$recent_authed_mail_ips_local_user ID=$recent_authed_mail_ips_local_uid S=$sender B=recent_authed_mail_ips_domain") } #Exim::log_write("!DEBUG! get_message_sender() got domain $domain from acl_c_recent_authed_mail_ips_text_entry"); } elsif ( Exim::expand_string('$received_protocol') eq 'local' ) { my $sender_ident = Exim::expand_string('$sender_ident'); $sender_ident =~ s/[\r\n\f]//g; my $used_vhost_owner_lookup = 0; if ( $sender_ident eq 'nobody' ) { if ($acl_c_vhost_owner) { $used_vhost_owner_lookup = 1; $sender_ident = uid2user($acl_c_vhost_owner); } } $sender = $sender_ident; $domain = getusersdomain($sender_ident) || _SENDER_SYSTEM; $sender_lookup_method = 'sender_ident via received_protocol == local: ' . $sender_ident . ( $used_vhost_owner_lookup ? ' : used vhost owner lookup from: ' . $acl_c_vhost_owner_url : '' ); # If the sender owns the domain they are sending # from we can trust it if ( length $sender && $sender !~ tr/\@// ) { ( $sender, $domain, $sender_lookup_method ) = resolve_authenticated_sender( $sender, $domain, $sender_lookup_method ); } #Exim::log_write("!DEBUG! get_message_sender() got domain $domain from local user ($sender_ident)"); } else { $mail_gid ||= int( ( getgrnam('mail') )[2] ); #Exim::log_write("!DEBUG! mailgid=$mail_gid == gid=$gid (uid=$uid)"); if ( $gid == $mail_gid ) { my ( $recent_authed_mail_ips_sender, $recent_authed_mail_ips_domain, $recent_authed_mail_ips_lookup_method ) = get_recent_authed_mail_ips_entry(); if ($recent_authed_mail_ips_domain) { $sender = $recent_authed_mail_ips_sender; $sender =~ s/[\r\n\f]//g; $domain = $recent_authed_mail_ips_domain; $sender_lookup_method = 'mailgid via get_recent_authed_mail_ips_entry: ' . $sender . "/$recent_authed_mail_ips_lookup_method"; #Exim::log_write("!DEBUG! get_message_sender() got domain $domain from get_recent_authed_mail_ips_entry() or sender_address_domain"); } $primary_hostname ||= Exim::expand_string('$primary_hostname'); if ( $domain && $domain eq $primary_hostname ) { $username = Exim::expand_string('$sender_address_local_part'); $sender = $username; $domain = getusersdomain($username) || _SENDER_SYSTEM; $sender_lookup_method = 'mailgid via primary_hostname' . "/$recent_authed_mail_ips_lookup_method"; } if ( !$domain ) { # If we cannot find the sender and it is not _SENDER_SYSTEM it is a redirected/forwarded message my $parent_domain = Exim::expand_string('$parent_domain'); my $parent_local_part = Exim::expand_string('$parent_local_part'); my $local_part = Exim::expand_string('$local_part'); my $delivery_domain = Exim::expand_string('$domain'); $parent_domain =~ s/[^\w\.\-\/]//g; $parent_local_part =~ s/[^\w\.\-\/]//g; $local_part =~ s/[^\w\.\-\/]//g; $delivery_domain =~ s/[^\w\.\-\/]//g; # If we have a parent_domain its probably a redirect if ( $parent_domain && ( $parent_domain ne $delivery_domain || $parent_local_part ne $local_part ) ) { # If the parent_domain is the primary_hostname its a localuser redirect if ( my $local_user = $parent_domain eq $primary_hostname ? $parent_local_part : getdomainowner($parent_domain) ) { my $local_uid = user2uid($local_user); my $redirected_domain = $parent_domain eq $primary_hostname ? getusersdomain($parent_local_part) : $parent_domain; if ($log) { Exim::log_write("SMTP connection identification D=$redirected_domain O=$parent_local_part\@$parent_domain E=$local_part\@$delivery_domain M=$message_exim_id U=$local_user ID=$local_uid B=redirect_resolver") } ; #replay for tailwatchd $domain = $redirected_domain; $sender = $parent_domain eq $primary_hostname ? $local_user : "$parent_local_part\@$parent_domain"; $sender_lookup_method = "redirect/forwarder owner $parent_local_part\@$parent_domain -> $local_part\@$delivery_domain"; } } } if ( !$domain ) { $sender_lookup_method = 'mailgid no entry from get_recent_authed_mail_ips_entry'; #Exim::log_write("!DEBUG! get_message_sender() failed to get the domain. However the sender domain claims to be $sender_address_domain"); } } else { # FROM A SHELL OR CGI $username = uid2user($uid); if ($username) { if ( $username eq 'nobody' ) { if ($acl_c_vhost_owner) { $username = uid2user($acl_c_vhost_owner); } $sender_lookup_method = 'uid via acl_c_vhost_owner from shell cgi: ' . $username . ' from: ' . $acl_c_vhost_owner_url; } else { $sender_lookup_method = 'uid via shell cgi: ' . $username; } $domain = getusersdomain($username) || _SENDER_SYSTEM; $sender = $username; } # If the sender owns the domain they are sending # from we can trust it if ( length $sender && $sender !~ tr/\@// ) { ( $sender, $domain, $sender_lookup_method ) = resolve_authenticated_sender( $sender, $domain, $sender_lookup_method ); } #Exim::log_write("!DEBUG! get_message_sender() got domain $domain from UID"); } } if ($domain) { $domain =~ s/[^\w\.\-\/]//g; $domain = lc $domain; $counted_domain = $domain; if ($sender) { $sender =~ tr/+%:/@/; $sender =~ s/[^\w\.\-\/\@]//g; if ( $sender eq 'mailman' ) { $is_mailman = 1; $domain = lc Exim::expand_string('$sender_address_domain'); $sender_lookup_method .= '/mailman'; $sender = 'mailman@' . $domain; $counted_domain = $domain if ( file_exists('/var/cpanel/email_send_limits/count_mailman') ); } } } $sender_lookup = $sender; if ( $log && $message_exim_id ) { $username ||= ( ( $sender =~ tr{@}{} ) ? getdomainowner( ( split( m{@}, $sender ) )[1] ) : $sender ); if ($username) { # Will log as 2017-05-26 13:42:22 1dEKBq-0007HB-6R Sender identification S=nick Exim::log_write("Sender identification U=$username D=$domain S=$sender"); #replay for tailwatchd } } return ( $sender, $domain, $counted_domain, $is_mailman ); } sub get_message_sender_address { return ( get_message_sender(@_) )[0]; } sub enforce_mail_permissions { $enforce_mail_permissions_data ? 1 : 0; } sub check_mail_permissions { $check_mail_permissions_domain = undef; #Exim::log_write("!DEBUG! running check_mail_permissions"); my $uid = int( Exim::expand_string('$originator_uid') ); $enforce_mail_permissions_data = ':fail: check_mail_permissions failed to complete or set a status'; $check_mail_permissions_result = ''; $check_mail_permissions_data = ':unknown:'; $check_mail_permissions_domain = ''; $check_mail_permissions_sender = ''; $check_mail_permissions_is_mailman = 0; $nobody_uid ||= user2uid('nobody'); my $acl_c_vhost_owner = ( split( m{:}, Exim::expand_string('$acl_c_vhost_owner') || '' ) )[0]; my $acl_c_vhost_owner_known_user = ( $acl_c_vhost_owner && $acl_c_vhost_owner != $nobody_uid ) ? 1 : 0; if ( $uid == $nobody_uid && !$acl_c_vhost_owner_known_user && file_exists('/etc/webspam') ) { $enforce_mail_permissions_data = ':fail: Mail sent by user nobody being discarded due to sender restrictions in WHM->Tweak Settings'; $check_mail_permissions_result = "uid ($uid) is the nobody_uid ($nobody_uid) and /etc/webspam exists"; # for tests (only set when enforce_mail_permissions_data is empty) return 'no'; } my $gid = int( Exim::expand_string('$originator_gid') ); #MAILTRAP if ( file_exists('/etc/eximmailtrap') ) { $mailtrap_gid ||= int( ( getgrnam('mailtrap') )[2] ); $nobody_gid ||= int( ( getgrnam('nobody') )[2] ); if ( $uid >= $nobody_uid && $gid >= $nobody_gid && $gid != $mailtrap_gid ) { $enforce_mail_permissions_data = ":fail: Gid $gid is not permitted to relay mail, or has directly called /usr/sbin/exim instead of /usr/sbin/sendmail."; return 'no'; } } #MAILTRAP if ( Exim::expand_string('$received_protocol') eq 'local' && isdemo($uid) ) { $enforce_mail_permissions_data = ":fail: User with uid $uid is a demo user. You cannot send mail if your account is in demo mode."; return 'no'; } my $message_exim_id = Exim::expand_string('$message_exim_id'); if ( !$message_exim_id && !Exim::expand_string('$sender_address') ) { $enforce_mail_permissions_data = ''; # permit normal acction #Exim::log_write("!DEBUG! check_mail_permissions called without sender_address set from $sender_host_address (rcount: $recipients_count)"); $check_mail_permissions_result = "webspam check, mailtrap check, demo check passed and no sender_address"; # for tests (only set when enforce_mail_permissions_data is empty) return 'no'; } # real_domain is the domain of the actual sender # domain is the domain we actually count the message against # Currently these are always the same except domain may be # rewritten if we are coming from a mailman list in order # to count against the owner of the list instead of the mailman # user assuming /var/cpanel/email_send_limits/count_mailman exists my ( $sender, $real_domain, $domain, $is_mailman ) = get_message_sender( $uid, $gid, 1 ); if ( $sender =~ m/^_archive\@/ ) { $enforce_mail_permissions_data = ":fail: Archive Users are not permitted to send email. Message discarded."; $check_mail_permissions_result = "get_message_sender returned an archive user"; return 'no'; } if ( !Cpanel::Server::Type::Role::MailRelay->is_enabled() ) { $enforce_mail_permissions_data = ":fail: This server does not relay mail."; $check_mail_permissions_result = "This server does not relay mail."; return 'no'; } if ( !$domain || $domain eq '' ) { my $sender_host_address = Exim::expand_string('$received_protocol') eq 'local' ? 'localhost' : Exim::expand_string('$sender_host_address'); my $recipients_count = Exim::expand_string('$recipients_count'); my $routed_domain = Exim::expand_string('$domain'); if ( $sender eq 'nobody' && file_exists('/etc/webspam') ) { Exim::log_write("check_mail_permissions could not determine the sender domain for a nobody message [routed_domain=$routed_domain message_exim_id=$message_exim_id sender_host_address=$sender_host_address recipients_count=$recipients_count]") if $recipients_count && !getdomainowner($routed_domain); $enforce_mail_permissions_data = ':fail: Mail sent by user nobody that cannot be linked to a user is being discarded due to sender restrictions in WHM->Tweak Settings'; $check_mail_permissions_result = "The sender of the message nobody and /etc/webspam exists"; # for tests (only set when enforce_mail_permissions_data is empty) } else { Exim::log_write("check_mail_permissions could not determine the sender domain [routed_domain=$routed_domain message_exim_id=$message_exim_id sender_host_address=$sender_host_address recipients_count=$recipients_count]") if $recipients_count && !getdomainowner($routed_domain); # If delivery is to a userdomain that its expected that we cannot get the sender domain $enforce_mail_permissions_data = ''; # permit normal acction $check_mail_permissions_result = "get_message_sender returned no domain"; # for tests (only set when enforce_mail_permissions_data is empty) } return 'no'; } else { if ( !$message_exim_id ) { #Exim::log_write("check_mail_permissions !DEBUG! got the domain ($domain) of a message before the message id!"); } } #Exim::log_write("check_mail_permissions !DEBUG! found sender domain of message: $message_exim_id to be $domain with sender [$sender]"); $check_mail_permissions_msgid = $message_exim_id if $message_exim_id; $check_mail_permissions_domain = $domain if $domain; $check_mail_permissions_sender = $sender if $sender; $check_mail_permissions_is_mailman = $is_mailman; if ( $domain && $domain ne _SENDER_SYSTEM ) { my $now; # Just before we check to see if we've exceeded the allowable mail counts for this domain, # check to see if we need to notify the admin about someone exceeding the warning level my $mail_count = get_current_emails_per_day($domain) + 1; # +1 for the one we're *about* to send, but haven't yet! my $emails_to_notify = get_email_daily_limit_notify(); if ( ( $emails_to_notify > 0 ) && ( $mail_count > $emails_to_notify ) ) { if ( !file_exists( '/var/cpanel/email_send_limits/daily_notify/' . $domain ) ) { create_daily_notify_touchfile($domain); Exim::log_write("check_mail_permissions Hit daily email notify limit for domain $domain"); } } if ( file_exists( '/var/cpanel/email_send_limits/max_deferfail_' . $domain ) ) { local $/; my $limit_data; if ( open( my $email_fh, '<', '/var/cpanel/email_send_limits/max_deferfail_' . $domain ) ) { $limit_data = readline($email_fh); close($email_fh); } my ( $currentmail, $maxmails, $percentage ) = $limit_data =~ /([0-9]+)\/([0-9]+)\s+\(([0-9]+)/; $currentmail ||= 'unknown'; $maxmails ||= 'unknown'; $percentage ||= 100; $enforce_mail_permissions_data = ":fail: Domain $domain has exceeded the max defers and failures per hour ($currentmail/$maxmails ($percentage\%)) allowed. Message discarded."; return 'no'; } elsif ( my $maxmails = getmaxemailsperhour($domain) ) { my $currentmail = get_current_emails_per_hour( $domain, ( $now ||= time() ) ); if ( $currentmail >= $maxmails ) { my $cutoff_percentage = get_email_send_limits_defer_cutoff(); my $percentage = int( ( $currentmail / $maxmails ) * 100 ); if ( $percentage >= $cutoff_percentage ) { $enforce_mail_permissions_data = ":fail: Domain $domain has exceeded the max emails per hour ($currentmail/$maxmails ($percentage\%)) allowed. Message discarded."; return 'no'; } else { increment_max_emails_per_hour( $domain, ( $now ||= time() ), $message_exim_id ); # need to count it because we will try it later # this will result in percentages above 100% which may be confusing however correct # this is how we decide to defer or fail the message return _check_mail_permission_defer_with_message("Domain $domain has exceeded the max emails per hour ($currentmail/$maxmails ($percentage\%)) allowed. $reattempt_message"); } } } if ( domain_has_outgoing_mail_suspended($domain) ) { # We already check this in the ACL, however if the sender domain # is forged we have to check it again here to ensure that # we are checking against the actual sender and not the # domain in the from: field $enforce_mail_permissions_data = ":fail: Domain $domain has an outgoing mail suspension. Message discarded."; return 'no'; } elsif ( domain_has_outgoing_mail_hold($domain) ) { track_held_message($domain); return _check_mail_permission_defer_with_message("Domain $domain has an outgoing mail hold. $reattempt_message"); } elsif ($sender) { if ( user_has_outgoing_mail_suspended($sender) ) { # We already check this in the ACL, however if the sender domain # is forged we have to check it again here to ensure that # we are checking against the actual sender and not the # domain in the from: field $enforce_mail_permissions_data = ":fail: Sender $sender has an outgoing mail suspension. Message discarded."; return 'no'; } elsif ( user_has_outgoing_mail_hold($sender) ) { track_held_message($sender); return _check_mail_permission_defer_with_message("Sender $sender has an outgoing mail hold. $reattempt_message"); } } } $enforce_mail_permissions_data = ''; # permit normal action $check_mail_permissions_result = "reached end of check_mail_permissions"; # for tests (only set when enforce_mail_permissions_data is empty) return 'no'; } sub _check_mail_permission_defer_with_message { my ($message) = @_; my $message_body = Exim::expand_string('$message_body'); my $message_body_size = Exim::expand_string('$message_body_size'); my $message_body_length = length($message_body); $check_mail_permissions_data = qq{# Exim filter\n\nunseen mail } . ( $check_mail_permissions_sender ? qq{to } . Cpanel::Encoder::Exim::unquoted_encode_string_literal($check_mail_permissions_sender) . qq{\n} : '' ) . q{subject "Mail delivery deferred: returning message to sender" } . q{from "Mail Delivery System <Mailer-Daemon@$primary_hostname>" } . q{text "This message was created automatically by mail delivery software.\n} . q{\n} . q{A message that you sent could not be delivered to one or more of its\n} . q{recipients. This is a temporary error. The following address(es) deferred:\n} . q{\n} . q{ $local_part@$domain\n} . qq{ $message} . q{\n\n} . q{------- This is a copy of the message, including all the headers. ------\n} . ( ( $message_body_length < $message_body_size ) ? ( q{------ The body of the message is $message_body_size characters long; only the first\n} . q{------ } . $message_body_length . q{ or so are included here.\n} ) : () ) . q{$message_headers\n\n} . q{$message_body"} . qq{\nfinish}; $enforce_mail_permissions_data = ":defer: \"$message\""; return 'yes'; } sub domain_has_outgoing_mail_hold { my ($domain) = @_; my $user = getdomainowner($domain); if ( $user && user_has_outgoing_mail_hold($user) ) { return 1; } return 0; } sub domain_has_outgoing_mail_suspended { my ($domain) = @_; my $user = getdomainowner($domain); if ( $user && user_has_outgoing_mail_suspended($user) ) { return 1; } return 0; } sub user_has_outgoing_mail_suspended { my ($user) = @_; if ( -e '/etc/outgoing_mail_suspended_users' ) { return user_exists_in_db( $user, '/etc/outgoing_mail_suspended_users' ); } return 0; } sub user_has_outgoing_mail_hold { my ($user) = @_; if ( -e '/etc/outgoing_mail_hold_users' ) { return user_exists_in_db( $user, '/etc/outgoing_mail_hold_users' ); } return 0; } sub check_outgoing_mail_suspended { if ( !Cpanel::Server::Type::Role::MailRelay->is_enabled() && Exim::expand_string('$sender_host_address') ) { $outgoing_mail_suspended_message = "This server does not relay mail."; return 1; } my $uid = int( Exim::expand_string('$originator_uid') ); my $gid = int( Exim::expand_string('$originator_gid') ); my ( $sender, $real_domain, $domain, $is_mailman ) = get_message_sender( $uid, $gid, 0 ); if ( $real_domain && $real_domain ne _SENDER_SYSTEM && domain_has_outgoing_mail_suspended($real_domain) ) { $outgoing_mail_suspended_message = "Outgoing mail from \"$real_domain\" has been suspended."; return 1; } elsif ( $sender && user_has_outgoing_mail_suspended($sender) ) { $outgoing_mail_suspended_message = "Outgoing mail from \"$sender\" has been suspended."; return 1; } return 0; } sub get_outgoing_mail_suspended_message { return $outgoing_mail_suspended_message; } sub increment_max_emails_per_hour_if_needed { # Exim::log_write("!DEBUG! increment_max_emails_per_hour_if_needed entered"); if ( $check_mail_permissions_domain && $check_mail_permissions_domain ne _SENDER_SYSTEM ) { if ( Exim::expand_string('${if first_delivery{1}{0}}') || ( $check_mail_permissions_msgid && _get_last_delivery_message($check_mail_permissions_msgid) =~ m/$reattempt_message/o ) ) { # if FIRST_DELIVERY or last line of msglog is our $reattempt_message # example == f@kos.net R=check_mail_permissions defer (-1): Domain pigdog.org has exceeded the max emails per hour (12/10 (120%)) allowed. Message will be reattempted later # we need to tell the next function to charge us for the message since it was deferred before and we did not get here # Exim::log_write("!DEBUG! increment_max_emails_per_hour=$check_mail_permissions_domain msgid=$check_mail_permissions_msgid"); increment_max_emails_per_hour( $check_mail_permissions_domain, time(), $check_mail_permissions_msgid ); } } return 'no'; } sub store_spam { my $sender_host_address = shift; my $spam_score = shift; my $now = time(); open( my $spam_fh, '>>', '/var/cpanel/spamstore' ); #uncomment to deploy # syswrite($spam_fh, $now . ':' . $sender_host_address . ':' . $spam_score . ":.\n"); close($spam_fh); } sub _get_last_delivery_message { my $message_exim_id = shift; my ( $last_message, $msglog_file, $msglog_size ); my $spool_directory = Exim::expand_string('$spool_directory'); my $spool_split_directory = substr( ( split( /-/, $message_exim_id ) )[0], -1, 1 ); if ( file_exists("$spool_directory/msglog/$spool_split_directory/$message_exim_id") ) { #split spool $msglog_size = ( stat(_) )[7]; $msglog_file = "$spool_directory/msglog/$spool_split_directory/$message_exim_id"; } elsif ( file_exists("$spool_directory/msglog/$message_exim_id") ) { #not split $msglog_size = ( stat(_) )[7]; $msglog_file = "$spool_directory/msglog/$message_exim_id"; } if ( $msglog_file && open( my $msg_log_fh, '<', $msglog_file ) ) { seek( $msg_log_fh, $msglog_size - 4096, 0 ) if $msglog_size > 8192; local $/; $last_message = ( split( /\n/, readline($msg_log_fh) ) )[-1]; } # Exim::log_write("!DEBUG! _get_last_delivery_message for [$message_exim_id] is $last_message"); return $last_message || ''; } sub resolve_authenticated_sender { my ( $sender, $domain, $sender_lookup_method ) = @_; my $sender_address = Exim::expand_string('$sender_address'); my $sender_address_domain = Exim::expand_string('$sender_address_domain'); # We only want to use the sender in the from header if they have already # authenticated with at least the permissions of the account my ( $from_h_sender, $from_h_localpart, $from_h_domain ) = _get_from_h_sender(); $primary_hostname ||= Exim::expand_string('$primary_hostname'); # The user expects to be able to just set the From: headers # we try to accomodate that first if they have permissions on the account if ( $from_h_domain eq $primary_hostname ) { $sender_lookup_method .= "/primary_hostname/system user"; } elsif ( $sender eq getdomainowner($from_h_domain) ) { $sender = $from_h_localpart . '@' . $from_h_domain; $domain = $from_h_domain; $sender_lookup_method .= "/from_h"; } # otherwise we fallback to the sender_address_domain elsif ( $sender eq getdomainowner($sender_address_domain) ) { $sender = $sender_address; $domain = $sender_address_domain; $sender_lookup_method .= "/sender_address_domain"; } else { # finally we accept that we don't know who sent it besdies the # authenticated user $sender_lookup_method .= "/only user confirmed/virtual account not confirmed"; } return ( $sender, $domain, $sender_lookup_method ); } sub resolve_vhost_owner { if ( file_exists('/var/cpanel/config/email/trust_x_php_script') ) { if ( my $x_php_script = Exim::expand_string('$h_x-php-script:') ) { #X-PHP-Script: <servername><php-self> for <remote-addr> #X-PHP-Script: www.example.com/~user/testapp/send-mail.php for 10.0.0.1 my ( $servername, $uri ) = split( m{/}, $x_php_script, 2 ); if ( $uri =~ m/^\/?\~([^\/\s]+)/ ) { my $http_user = $1; my $uid = user2uid($http_user); Exim::log_write("nobody send identification H=localhost A=127.0.0.1 U=$http_user ID=$uid B=acl_c_vhost_owner M=trust_x_php_script"); return $uid . ':' . '//' . $servername . '/' . $uri . ' '; } elsif ( my $http_user = getdomainowner($servername) ) { my $uid = user2uid($http_user); Exim::log_write("nobody send identification H=localhost A=127.0.0.1 U=$http_user ID=$uid B=acl_c_vhost_owner M=trust_x_php_script"); return $uid . ':' . '//' . $servername . '/' . $uri . ' '; } } } if ( file_exists('/var/cpanel/config/email/query_apache_for_nobody_senders') ) { # Lets lookup the real uid by querying apache require Cpanel::ProcessInfo; require Cpanel::ApacheServerStatus; my $server_status = Cpanel::ApacheServerStatus->new(); my $httpd_pid; my $http_status_data; my $current_pid = $$; while ( ( $current_pid = Cpanel::ProcessInfo::get_parent_pid($current_pid) ) && $current_pid != 1 ) { if ( my $status_data = $server_status->get_status_by_pid($current_pid) ) { $httpd_pid = $current_pid; $http_status_data = $status_data; last; } } if ($http_status_data) { my $uri = ( split( /\s+/, $http_status_data->{'request'} ) )[1]; if ( $uri =~ m/^\/?\~([^\/\s]+)/ ) { my $http_user = $1; my $uid = user2uid($http_user); Exim::log_write("nobody send identification H=localhost A=127.0.0.1 U=$http_user ID=$uid B=acl_c_vhost_owner M=query_apache_for_nobody_senders"); return $uid . ':' . '//' . $http_status_data->{'vhost'} . $uri . ' '; } elsif ( my $http_user = getdomainowner( $http_status_data->{'vhost'} ) ) { my $uid = user2uid($http_user); Exim::log_write("nobody send identification H=localhost A=127.0.0.1 U=$http_user ID=$uid B=acl_c_vhost_owner M=query_apache_for_nobody_senders"); return $uid . ':' . '//' . $http_status_data->{'vhost'} . $uri . ' '; } } } return; } # Obtain the from header from the message # We fallback to the envelope sender if there # is no from header set (ie sendmail -bt or missing From header) sub _get_from_h_sender { my $from_h_domain = Exim::expand_string('${domain:$h_from:}'); my $from_h_local_part = Exim::expand_string('${local_part:$h_from:}'); if ( length $from_h_local_part ) { if ( length $from_h_domain ) { return ( $from_h_local_part . '@' . $from_h_domain, $from_h_local_part, $from_h_domain ); } else { $primary_hostname ||= Exim::expand_string('$primary_hostname'); return ( $from_h_local_part . '@' . $primary_hostname, $from_h_local_part, $primary_hostname ); } } else { # Handle fallback to sender_address when message is missing a from header my $sender_address_domain = Exim::expand_string('$sender_address_domain'); my $sender_address_local_part = Exim::expand_string('$sender_address_local_part'); return ( $sender_address_local_part . '@' . $sender_address_domain, $sender_address_local_part, $sender_address_domain ); } } my $email_holds_dir = '/var/cpanel/email_holds'; sub track_held_message { my ($holder) = @_; if ( -1 != index( $holder, '/' ) ) { warn "Holder “$holder” should not have “/” in it!"; $holder =~ s/\///g; #jic } my $message_exim_id = Exim::expand_string('$message_exim_id'); _check_hold_dir($holder); my $path = "$email_holds_dir/track/$holder/$message_exim_id"; if ( !-e $path ) { if ( $! == _ENOENT() ) { open( my $fh, '>>', $path ) or do { warn "open(>>, $path): $!"; }; } else { warn "stat($path): $!"; } } return 1; } sub _mkdir_if_not_exists_or_warn { my ( $path, $mode ) = @_; mkdir( $path, $mode ) or do { if ( $! != _EEXIST() ) { warn "mkdir($path, $mode): $!"; } return undef; }; return 1; } sub _check_hold_dir { my ($holder) = @_; if ( !-e "$email_holds_dir/track/$holder" ) { if ( $! == _ENOENT() ) { _mkdir_if_not_exists_or_warn( $email_holds_dir, 0751 ); _mkdir_if_not_exists_or_warn( "$email_holds_dir/track", 0750 ); _mkdir_if_not_exists_or_warn( "$email_holds_dir/track/$holder", 0750 ); } else { warn "stat($email_holds_dir/track/$holder): $!"; } } return; } =head2 maskdir($dir) This function converts a path on the system to a path relative to the users home directory that it contains. The relative path is prefixed with the user's primary domain in the below format: domain.tld:/public_html/cgi-bin/xyz.cgi If the path is not contained within a user's home directory, the path is returned without modification. =cut sub maskdir { my ($dir) = @_; # Try the user first my $maskeddir = $dir; my ($likely_user) = ( split( m{/}, $dir ) )[2]; if ( my $likely_homedir = gethomedir($likely_user) ) { chop $likely_homedir if substr( $likely_homedir, -1 ) eq '/'; if ( rindex( $dir, "$likely_homedir/", 0 ) == 0 ) { substr( $maskeddir, 0, length($likely_homedir), getusersdomain($likely_user) . ":" ); return $maskeddir; } } # Next try all users in /etc/passwd if ( open my $passwd_fh, '<', "/etc/passwd" ) { while ( readline($passwd_fh) ) { my ( $homedir, $uid, $user ) = ( split( /:/, $_ ) )[ 0, 2, 5 ]; next if $uid < 100 || length $homedir < 3; chop $homedir if substr( $homedir, -1 ) eq '/'; if ( rindex( $dir, "$homedir/", 0 ) == 0 ) { substr( $maskeddir, 0, length($homedir), getusersdomain($user) . ":" ); return $maskeddir; } } } else { warn "open(/etc/passwd): $!"; } return $dir; } sub extract_hosts_from_route_list_item { my $item = shift; my (undef, $hosts, undef) = Exim::parse_route_item($item); return $hosts; } sub convert_to_hostlist_item { my ($item, $separator) = @_; $separator //= '\n'; $item =~ s/^\s+//; $item =~ s/\s+$//; # Ignore group separator: if ($item eq '+') { $item = ''; } # Extract bracketed IP address: elsif ( $item !~ s/^\[(\S*)\]:\d+$/$1/ ) { # If nothing subbed, what's left is an unbracketed IPv4 or a hostname. # Remove port if present: $item =~ s/:\d+$//; # Finally, if the hostname specified /mx, do a lookup of its MX records and sub in the entire list: if ($item =~ s{^(\S+)/mx$}{$1}i) { $item = Exim::expand_string('${lookup dnsdb{>' . $separator . ' mxh=' . $item . '}{$value}}'); } } return $item; } sub get_suspended_shell { my ($user) = @_; my $passwd_file_shell = Exim::expand_string( '${extract{6}{:}{${lookup passwd{' . Cpanel::Encoder::Exim::unquoted_encode_string_literal($user) . '}}}}' ); if ( !length($passwd_file_shell) ) { return ''; } if ( $passwd_file_shell ne '/bin/false' ) { return $passwd_file_shell; } if ( open my $fh, '<', "/var/cpanel/suspendinfo/${user}" ) { while ( my $ln = readline($fh) ) { if ( $ln =~ m{\Ashell=\s*(\S+)} ) { close $fh; return $1; } } close $fh; } return '/usr/local/cpanel/bin/noshell'; } # Untaint a string for exim. This is not a perl untaint sub untaint { return $_[0]; } require Cpanel::Encoder::Exim; require Cpanel::Server::Type::Role::MailRelay; 1; BEGIN { # Suppress load of all of these at earliest point. $INC{'cPstrict.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/Encoder/Exim.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/ExceptionMessage.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/Locale/Utils/Fallback.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/ExceptionMessage/Raw.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/LoadModule/Utils.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/ScalarUtil.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/Exception/CORE.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/Pack.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/Pack/Template.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/Validate/IP/v4.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/Validate/IP.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/Validate/IP/Expand.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/IP/Expand.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/Linux/Netlink.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/Linux/Proc/Net/Tcp.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/Ident.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/Autodie.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/Autodie/CORE/exists.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/Autodie/CORE/exists_nofollow.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/Autodie/More/Lite.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/Services/Enabled/Spamd.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/FileUtils/Dir.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/DKIM/ValidityCache.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/Context.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/ProcessInfo.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/Fcntl/Constants.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/Socket/Constants.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/Hulk/Constants.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/ApacheServerStatus.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/Server/Type.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/Server/Type/Profile/Constants.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/LoadModule.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/Server/Type/Profile.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/Server/Type/Role/EnabledCache.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/Server/Type/Role.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/Server/Type/Role/TouchFileRole.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; $INC{'Cpanel/Server/Type/Role/MailRelay.pm'} = '/usr/local/cpanel/tmp/exim.local.build.pl.static'; } { # --- BEGIN cPstrict package cPstrict; # cpanel - cPstrict.pm 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 use strict; use warnings; =pod This is importing the following to your namespace use strict; use warnings; use v5.30; use feature 'signatures'; no warnings 'experimental::signatures'; =cut sub import { # auto import strict and warnings to our caller warnings->import(); strict->import(); require feature; feature->import( ':5.30', 'signatures' ); warnings->unimport('experimental::signatures'); return; } 1; } # --- END cPstrict { # --- BEGIN Cpanel/Encoder/Exim.pm package Cpanel::Encoder::Exim; my %encodes = ( q{\\} => q{\\\\\\\\}, #\ -> \\\\ q{"} => q{\\"}, #" -> \" q{$} => q{\\\\$}, #$ -> \\$ "\x0a" => q{\\n}, #newline -> \n "\x0d" => q{\\r}, #carriage return -> \r "\x09" => q{\\t}, #tab => \t ); sub encode_string_literal { return if !defined $_[0]; return q{"} . join( q{}, map { $encodes{$_} || $_ } split( m{}, $_[0] ) ) . q{"}; } sub unquoted_encode_string_literal { my $string = shift; return if !defined $string; $string =~ s/\\N/\\N\\\\N\\N/g; # Only use / here for perl compat return "\\N$string\\N"; } 1; } # --- END Cpanel/Encoder/Exim.pm { # --- BEGIN Cpanel/ExceptionMessage.pm package Cpanel::ExceptionMessage; use strict; # use Cpanel::Exception (); *load_perl_module = \&Cpanel::Exception::load_perl_module; 1; } # --- END Cpanel/ExceptionMessage.pm { # --- BEGIN Cpanel/Locale/Utils/Fallback.pm package Cpanel::Locale::Utils::Fallback; use strict; use warnings; sub interpolate_variables { my ( $str, @maketext_opts ) = @_; my $c = 1; my %h = map { $c++, $_ } @maketext_opts; $str =~ s{(\[(?:[^_]+,)?_([0-9])+\])}{$h{$2}}g; return $str; } 1; } # --- END Cpanel/Locale/Utils/Fallback.pm { # --- BEGIN Cpanel/ExceptionMessage/Raw.pm package Cpanel::ExceptionMessage::Raw; use strict; use warnings; # use Cpanel::ExceptionMessage(); our @ISA; BEGIN { push @ISA, qw(Cpanel::ExceptionMessage); } # use Cpanel::Locale::Utils::Fallback (); sub new { my ( $class, $str ) = @_; my $str_copy = $str; return bless( \$str_copy, $class ); } sub to_string { my ($self) = @_; return $$self; } sub get_language_tag { return 'en'; } BEGIN { *Cpanel::ExceptionMessage::Raw::convert_localized_to_raw = *Cpanel::Locale::Utils::Fallback::interpolate_variables; *Cpanel::ExceptionMessage::Raw::to_locale_string = *Cpanel::ExceptionMessage::Raw::to_string; *Cpanel::ExceptionMessage::Raw::to_en_string = *Cpanel::ExceptionMessage::Raw::to_string; } 1; } # --- END Cpanel/ExceptionMessage/Raw.pm { # --- BEGIN Cpanel/LoadModule/Utils.pm package Cpanel::LoadModule::Utils; use strict; use warnings; sub module_is_loaded { my $p = module_path( $_[0] ); return 0 unless defined $p; return defined $INC{$p} ? 1 : 0; } sub module_path { my ($module_name) = @_; if ( defined $module_name && length($module_name) ) { substr( $module_name, index( $module_name, '::' ), 2, '/' ) while index( $module_name, '::' ) > -1; $module_name .= '.pm' unless substr( $module_name, -3 ) eq '.pm'; } return $module_name; } sub is_valid_module_name { return $_[0] =~ m/\A[A-Za-z_]\w*(?:(?:'|::)\w+)*\z/ ? 1 : 0; } 1; } # --- END Cpanel/LoadModule/Utils.pm { # --- BEGIN Cpanel/ScalarUtil.pm package Cpanel::ScalarUtil; use strict; use warnings; sub blessed { return ref( $_[0] ) && UNIVERSAL::isa( $_[0], 'UNIVERSAL' ) || undef; } 1; } # --- END Cpanel/ScalarUtil.pm { # --- BEGIN Cpanel/Exception/CORE.pm package Cpanel::Exception::CORE; 1; package Cpanel::Exception; use strict; BEGIN { $INC{'Cpanel/Exception.pm'} = '__BYPASSED__'; } our $_SUPPRESS_STACK_TRACES = 0; our $_EXCEPTION_MODULE_PREFIX = 'Cpanel::Exception'; our $IN_EXCEPTION_CREATION = 0; our $_suppressed_msg = '__STACK_TRACE_SUPPRESSED__YOU_SHOULD_NEVER_SEE_THIS_MESSAGE__'; my $PACKAGE = 'Cpanel::Exception'; my $locale; my @ID_CHARS = qw( a b c d e f g h j k m n p q r s t u v w x y z 2 3 4 5 6 7 8 9 ); my $ID_LENGTH = 6; # use Cpanel::ExceptionMessage::Raw (); # use Cpanel::LoadModule::Utils (); use constant _TRUE => 1; use overload ( '""' => \&__spew, bool => \&_TRUE, fallback => 1, ); BEGIN { die "Cannot compile Cpanel::Exception::CORE" if $INC{'B/C.pm'} && $0 !~ m{cpkeyclt|cpsrvd\.so|t/large}; } sub _init { return 1 } # legacy sub create { my ( $exception_type, @args ) = @_; _init(); if ($IN_EXCEPTION_CREATION) { _load_cpanel_carp(); die 'Cpanel::Carp'->can('safe_longmess')->("Attempted to create a “$exception_type” exception with arguments “@args” while creating exception “$IN_EXCEPTION_CREATION->[0]” with arguments “@{$IN_EXCEPTION_CREATION->[1]}”."); } local $IN_EXCEPTION_CREATION = [ $exception_type, \@args ]; if ( $exception_type !~ m/\A[A-Za-z0-9_]+(?:\:\:[A-Za-z0-9_]+)*\z/ ) { die "Invalid exception type: $exception_type"; } my $perl_class; if ( $exception_type eq __PACKAGE__ ) { $perl_class = $exception_type; } else { $perl_class = "${_EXCEPTION_MODULE_PREFIX}::$exception_type"; } _load_perl_module($perl_class) unless $perl_class->can('new'); if ( $args[0] && ref $args[0] eq 'ARRAY' && scalar @{ $args[0] } > 1 ) { $args[0] = { @{ $args[0] } }; } return $perl_class->new(@args); } sub create_raw { my ( $class, $msg, @extra_args ) = @_; _init(); my $msg_obj = 'Cpanel::ExceptionMessage::Raw'->new($msg); if ( $class =~ m<\A(?:\Q${_EXCEPTION_MODULE_PREFIX}::\E)?Collection\z> ) { die "Use create('Collection', ..) to create a Cpanel::Exception::Collection object."; } return create( $class, $msg_obj, @extra_args ); } sub _load_perl_module { my ($module) = @_; local ( $!, $@ ); if ( !defined $module ) { die __PACKAGE__->new( 'Cpanel::ExceptionMessage::Raw'->new("load_perl_module requires a module name.") ); } return 1 if Cpanel::LoadModule::Utils::module_is_loaded($module); my $module_name = $module; $module_name =~ s{\.pm$}{}; if ( !Cpanel::LoadModule::Utils::is_valid_module_name($module_name) ) { die __PACKAGE__->new( 'Cpanel::ExceptionMessage::Raw'->new("load_perl_module requires a valid module name: '$module_name'.") ); } { eval qq{use $module (); 1 } or die __PACKAGE__->new( 'Cpanel::ExceptionMessage::Raw'->new("load_perl_module cannot load '$module_name': $@") ) } return 1; } sub new { my ( $class, @args ) = @_; @args = grep { defined } @args; my $self = {}; bless $self, $class; if ( ref $args[-1] eq 'HASH' ) { $self->{'_metadata'} = pop @args; } if ( defined $self->{'_metadata'}->{'longmess'} ) { $self->{'_longmess'} = &{ $self->{'_metadata'}->{'longmess'} }($self) if $self->{'_metadata'}->{'longmess'}; } elsif ($_SUPPRESS_STACK_TRACES) { $self->{'_longmess'} = $_suppressed_msg; } else { if ( !$INC{'Carp.pm'} ) { _load_carp(); } $self->{'_longmess'} = scalar do { local $Carp::CarpInternal{'Cpanel::Exception'} = 1; local $Carp::CarpInternal{$class} = 1; 'Carp'->can('longmess')->(); }; } _init(); $self->{'_auxiliaries'} = []; if ( UNIVERSAL::isa( $args[0], 'Cpanel::ExceptionMessage' ) ) { $self->{'_message'} = shift @args; } else { my @mt_args; if ( @args && !ref $args[0] ) { @mt_args = ( shift @args ); if ( ref $args[0] eq 'ARRAY' ) { push @mt_args, @{ $args[0] }; } } else { $self->{'_orig_mt_args'} = $args[0]; my $phrase = $self->_default_phrase( $args[0] ); if ($phrase) { if ( ref $phrase ) { @mt_args = $phrase->to_list(); } else { $self->{'_message'} = Cpanel::ExceptionMessage::Raw->new($phrase); return $self; } } } if ( my @extras = grep { !ref } @args ) { die __PACKAGE__->new( 'Cpanel::ExceptionMessage::Raw'->new("Extra scalar(s) passed to $PACKAGE! (@extras)") ); } if ( !length $mt_args[0] ) { die __PACKAGE__->new( 'Cpanel::ExceptionMessage::Raw'->new("No args passed to $PACKAGE constructor!") ); } $self->{'_mt_args'} = \@mt_args; } return $self; } sub get_string { my ( $exc, $no_id_yn ) = @_; return get_string_no_id($exc) if $no_id_yn; return _get_string( $exc, 'to_string' ); } sub get_string_no_id { my ($exc) = @_; return _get_string( $exc, 'to_string_no_id' ); } sub _get_string { my ( $exc, $cp_exc_stringifier_name ) = @_; return $exc if !ref $exc; { local $@; my $ret = eval { $exc->$cp_exc_stringifier_name() }; return $ret if defined $ret && !$@ && !ref $ret; } if ( ref $exc eq 'HASH' && $exc->{'message'} ) { return $exc->{'message'}; } if ( $INC{'Cpanel/YAML.pm'} ) { local $@; my $ret = eval { 'Cpanel::YAML'->can('Dump')->($exc); }; return $ret if defined $ret && !$@; } if ( $INC{'Cpanel/JSON.pm'} ) { local $@; my $ret = eval { 'Cpanel::JSON'->can('Dump')->($exc); }; return $ret if defined $ret && !$@; } return $exc; } sub _create_id { srand(); return join( q<>, map { $ID_CHARS[ int rand( 0 + @ID_CHARS ) ]; } ( 1 .. $ID_LENGTH ), ); } sub get_stack_trace_suppressor { return Cpanel::Exception::_StackTraceSuppression->new(); } sub set_id { my ( $self, $new_id ) = @_; $self->{'_id'} = $new_id; return $self; } sub id { my ($self) = @_; return $self->{'_id'} ||= _create_id(); } sub set { my ( $self, $key ) = @_; $self->{'_metadata'}{$key} = $_[2]; if ( exists $self->{'_orig_mt_args'} ) { my $phrase = $self->_default_phrase( $self->{'_orig_mt_args'} ); if ($phrase) { if ( ref $phrase ) { $self->{'_mt_args'} = [ $phrase->to_list() ]; undef $self->{'_message'}; } else { $self->{'_message'} = Cpanel::ExceptionMessage::Raw->new($phrase); } } } return $self; } sub get { my ( $self, $key ) = @_; my $v = $self->{'_metadata'}{$key}; if ( my $reftype = ref $v ) { local $@; if ( $reftype eq 'HASH' ) { $v = { %{$v} }; # shallow copy } elsif ( $reftype eq 'ARRAY' ) { $v = [ @{$v} ]; # shallow copy } elsif ( $reftype eq 'SCALAR' ) { $v = \${$v}; # shallow copy } else { local ( $@, $! ); require Cpanel::ScalarUtil; if ( $reftype ne 'GLOB' && !Cpanel::ScalarUtil::blessed($v) ) { warn if !eval { _load_perl_module('Clone') if !$INC{'Clone.pm'}; $v = 'Clone'->can('clone')->($v); }; } } } return $v; } sub get_all_metadata { my $self = shift; my %metadata_copy; for my $key ( keys %{ $self->{'_metadata'} } ) { $metadata_copy{$key} = $self->get($key); } return \%metadata_copy; } my $loaded_LocaleString; sub _require_LocaleString { return $loaded_LocaleString ||= do { local $@; eval 'require Cpanel::LocaleString; 1;' or die $@; ## no critic qw(BuiltinFunctions::ProhibitStringyEval) - # PPI NO PARSE - load on demand 1; }; } my $loaded_ExceptionMessage_Locale; sub _require_ExceptionMessage_Locale { return $loaded_ExceptionMessage_Locale ||= do { local $@; eval 'require Cpanel::ExceptionMessage::Locale; 1;' or die $@; ## no critic qw(BuiltinFunctions::ProhibitStringyEval) - # PPI NO PARSE - load on demand 1; }; } sub _default_phrase { _require_LocaleString(); return 'Cpanel::LocaleString'->new( 'An unknown error in the “[_1]” package has occurred.', scalar ref $_[0] ); # PPI NO PARSE - loaded above } sub longmess { my ($self) = @_; return '' if $self->{'_longmess'} eq $_suppressed_msg; _load_cpanel_carp() if !$INC{'Cpanel/Carp.pm'}; return Cpanel::Carp::sanitize_longmess( $self->{'_longmess'} ); } sub to_string { my ($self) = @_; return _apply_id_prefix( $self->id(), $self->to_string_no_id() ); } sub to_string_no_id { my ($self) = @_; my $string = $self->to_locale_string_no_id(); if ( $self->_message()->get_language_tag() ne 'en' ) { my $en_string = $self->to_en_string_no_id(); $string .= "\n$en_string" if ( $en_string ne $string ); } return $string; } sub _apply_id_prefix { my ( $id, $msg ) = @_; return sprintf "(XID %s) %s", $id, $msg; } sub to_en_string { my ($self) = @_; return _apply_id_prefix( $self->id(), $self->to_en_string_no_id() ); } sub to_en_string_no_id { my ($self) = @_; return $self->_message()->to_en_string() . $self->_stringify_auxiliaries('to_en_string'); } sub to_locale_string { my ($self) = @_; return _apply_id_prefix( $self->id(), $self->to_locale_string_no_id() ); } sub to_locale_string_no_id { my ($self) = @_; return $self->_message()->to_locale_string() . $self->_stringify_auxiliaries('to_locale_string'); } sub add_auxiliary_exception { my ( $self, $aux ) = @_; return push @{ $self->{'_auxiliaries'} }, $aux; } sub get_auxiliary_exceptions { my ($self) = @_; die 'List context only!' if !wantarray; #Can’t use Cpanel::Context return @{ $self->{'_auxiliaries'} }; } sub __spew { my ($self) = @_; return $self->_spew(); } sub _spew { my ($self) = @_; return ref($self) . '/' . join "\n", $self->to_string() || '<no message>', $self->longmess() || (); } sub _stringify_auxiliaries { my ( $self, $method ) = @_; my @lines; if ( @{ $self->{'_auxiliaries'} } ) { local $@; _require_LocaleString(); my $intro = 'Cpanel::LocaleString'->new( 'The following additional [numerate,_1,error,errors] occurred:', 0 + @{ $self->{'_auxiliaries'} } ); # PPI NO PARSE - required above if ( $method eq 'to_locale_string' ) { push @lines, _locale()->makevar( $intro->to_list() ); } elsif ( $method eq 'to_en_string' ) { push @lines, _locale()->makethis_base( $intro->to_list() ); } else { die "Invalid method: $method"; } push @lines, map { UNIVERSAL::isa( $_, __PACKAGE__ ) ? $_->$method() : $_ } @{ $self->{'_auxiliaries'} }; } return join q<>, map { "\n$_" } @lines; } *TO_JSON = \&to_string; sub _locale { return $locale ||= do { local $@; eval 'require Cpanel::Locale; 1;' or die $@; 'Cpanel::Locale'->get_handle(); # hide from perlcc }; } sub _reset_locale { return undef $locale; } sub _load_carp { if ( !$INC{'Carp.pm'} ) { local $@; eval 'require Carp; 1;' or die $@; ## no critic qw(BuiltinFunctions::ProhibitStringyEval) -- hide from perlcc } return; } sub _load_cpanel_carp { if ( !$INC{'Cpanel/Carp.pm'} ) { local $@; eval 'require Cpanel::Carp; 1;' or die $@; ## no critic qw(BuiltinFunctions::ProhibitStringyEval) -- hide from perlcc } return; } sub _message { my ($self) = @_; return $self->{'_message'} if $self->{'_message'}; local $!; if ($Cpanel::Exception::LOCALIZE_STRINGS) { # the default _require_ExceptionMessage_Locale(); return ( $self->{'_message'} ||= 'Cpanel::ExceptionMessage::Locale'->new( @{ $self->{'_mt_args'} } ) ); # PPI NO PARSE - required above } return ( $self->{'_message'} ||= Cpanel::ExceptionMessage::Raw->new( Cpanel::ExceptionMessage::Raw::convert_localized_to_raw( @{ $self->{'_mt_args'} } ) ) ); } package Cpanel::Exception::_StackTraceSuppression; sub new { my ($class) = @_; $Cpanel::Exception::_SUPPRESS_STACK_TRACES++; return bless [], $class; } sub DESTROY { $Cpanel::Exception::_SUPPRESS_STACK_TRACES--; return; } 1; } # --- END Cpanel/Exception/CORE.pm { # --- BEGIN Cpanel/Pack.pm package Cpanel::Pack; use strict; sub new { my ( $class, $template_ar ) = @_; if ( @$template_ar % 2 ) { die "Cpanel::Pack::new detected an odd number of elements in hash assignment!"; } my $self = bless { 'template_str' => '', 'keys' => [], }, $class; my $ti = 0; while ( $ti < $#$template_ar ) { push @{ $self->{'keys'} }, $template_ar->[$ti]; $self->{'template_str'} .= $template_ar->[ 1 + $ti ]; $ti += 2; } return $self; } sub unpack_to_hashref { ## no critic (RequireArgUnpacking) my %result; @result{ @{ $_[0]->{'keys'} } } = unpack( $_[0]->{'template_str'}, $_[1] ); return \%result; } sub pack_from_hashref { my ( $self, $opts_ref ) = @_; no warnings 'uninitialized'; return pack( $self->{'template_str'}, @{$opts_ref}{ @{ $self->{'keys'} } } ); } sub sizeof { my ($self) = @_; return ( $self->{'sizeof'} ||= length pack( $self->{'template_str'}, () ) ); } sub malloc { my ($self) = @_; return pack( $self->{'template_str'} ); } 1; } # --- END Cpanel/Pack.pm { # --- BEGIN Cpanel/Pack/Template.pm package Cpanel::Pack::Template; use strict; use warnings; use constant PACK_TEMPLATE_INT => 'i'; use constant PACK_TEMPLATE_UNSIGNED_INT => 'i!'; use constant PACK_TEMPLATE_UNSIGNED_LONG => 'L!'; use constant PACK_TEMPLATE_U32 => 'L'; use constant U32_BYTES_LENGTH => 4; use constant PACK_TEMPLATE_U16 => 'S'; use constant U16_BYTES_LENGTH => 2; use constant PACK_TEMPLATE_U8 => 'C'; use constant U8_BYTES_LENGTH => 1; use constant PACK_TEMPLATE_BE16 => 'n'; use constant PACK_TEMPLATE_BE32 => 'N'; 1; } # --- END Cpanel/Pack/Template.pm { # --- BEGIN Cpanel/Validate/IP/v4.pm package Cpanel::Validate::IP::v4; use strict; use warnings; sub is_valid_ipv4 { my ($ip) = @_; return unless $ip; # False scalars are never an _[0]. my @segments = split /\./, $ip, -1; return unless scalar @segments == 4; my $octet_index; for my $octet_value (@segments) { return if !_valid_octet( $octet_value, ++$octet_index ); } return 1; } sub is_valid_cidr4 { my ($ip) = @_; return unless defined $ip && $ip; my ( $ip4, $mask ) = split /\//, $ip; return if !defined $mask || !length $mask || $mask =~ tr/0-9//c; return is_valid_ipv4($ip4) && 0 < $mask && $mask <= 32; } sub _valid_octet { my ( $octet_value, $octet_index ) = @_; return ( !length $octet_value || # $octet_value =~ tr/0-9//c || # $octet_value > 255 || # ( substr( $octet_value, 0, 1 ) == 0 && length($octet_value) > 1 ) || # Only dec values are permitted $octet_index == 1 && length($octet_value) && !$octet_value # First oct can't be zero. ) ? 0 : 1; } 1; } # --- END Cpanel/Validate/IP/v4.pm { # --- BEGIN Cpanel/Validate/IP.pm package Cpanel::Validate::IP; use strict; use warnings; # use Cpanel::Validate::IP::v4 (); sub is_valid_ipv6 { my ($ip) = @_; return unless defined $ip && $ip; if ( ( substr( $ip, 0, 1 ) eq ':' && substr( $ip, 1, 1 ) ne ':' ) || ( substr( $ip, -1, 1 ) eq ':' && substr( $ip, -2, 1 ) ne ':' ) ) { return; # Can't have single : on front or back } my @seg = split /:/, $ip, -1; # -1 to keep trailing empty fields shift @seg if $seg[0] eq ''; pop @seg if $seg[-1] eq ''; my $max = 8; if ( index( $seg[-1], '.' ) > -1 ) { return unless Cpanel::Validate::IP::v4::is_valid_ipv4( pop @seg ); $max -= 2; } my $cmp; for my $seg (@seg) { if ( !defined $seg || $seg eq '' ) { return if $cmp; ++$cmp; next; } return if $seg =~ tr/0-9a-fA-F//c || length $seg == 0 || length $seg > 4; } if ($cmp) { return ( @seg && @seg <= $max ) && 1; # true returned as 1 } return $max == @seg; } sub is_valid_ipv6_prefix { my ($ip) = @_; return unless $ip; my ( $ip6, $mask ) = split /\//, $ip; return unless defined $mask; return if !length $mask || $mask =~ tr/0-9//c; return is_valid_ipv6($ip6) && 0 < $mask && $mask <= 128; } sub is_valid_ip { return !defined $_[0] ? undef : index( $_[0], ':' ) > -1 ? is_valid_ipv6(@_) : Cpanel::Validate::IP::v4::is_valid_ipv4(@_); } sub ip_version { return 4 if Cpanel::Validate::IP::v4::is_valid_ipv4(@_); return 6 if is_valid_ipv6(@_); return; } sub is_valid_ip_cidr_or_prefix { return unless defined $_[0]; if ( $_[0] =~ tr/:// ) { return $_[0] =~ tr{/}{} ? is_valid_ipv6_prefix(@_) : is_valid_ipv6(@_); } return $_[0] =~ tr{/}{} ? Cpanel::Validate::IP::v4::is_valid_cidr4(@_) : Cpanel::Validate::IP::v4::is_valid_ipv4(@_); } sub is_valid_ip_range_cidr_or_prefix { my $str = shift; return 0 if !$str; return 1 if is_valid_ip_cidr_or_prefix($str); my @pieces = split /-/, $str, 2; return 1 if 2 == grep { defined($_) } map { Cpanel::Validate::IP::v4::is_valid_ipv4($_) } @pieces; return 1 if 2 == grep { defined($_) } map { is_valid_ipv6($_) } @pieces; return 0; } 1; } # --- END Cpanel/Validate/IP.pm { # --- BEGIN Cpanel/Validate/IP/Expand.pm package Cpanel::Validate::IP::Expand; use strict; use warnings; # use Cpanel::Validate::IP (); # use Cpanel::Validate::IP::v4 (); sub normalize_ipv4 { return unless Cpanel::Validate::IP::v4::is_valid_ipv4( $_[0] ); return join '.', map { $_ + 0 } split /\./, $_[0]; } sub expand_ipv6 { my $ip = shift; return unless Cpanel::Validate::IP::is_valid_ipv6($ip); return $ip if length $ip == 39; # already expanded my @seg = split /:/, $ip, -1; $seg[0] = '0000' if !length $seg[0]; $seg[-1] = '0000' if !length $seg[-1]; if ( $seg[-1] =~ tr{.}{} && Cpanel::Validate::IP::v4::is_valid_ipv4( $seg[-1] ) ) { my @ipv4 = split /\./, normalize_ipv4( pop @seg ); push @seg, sprintf( '%04x', ( $ipv4[0] << 8 ) + $ipv4[1] ), sprintf( '%04x', ( $ipv4[2] << 8 ) + $ipv4[3] ); } my @exp; for my $seg (@seg) { if ( !length $seg ) { my $count = scalar(@seg) - scalar(@exp); while ( $count + scalar(@exp) <= 8 ) { push @exp, '0000'; } } else { push @exp, sprintf( '%04x', hex $seg ); } } return join ':', @exp; } sub normalize_ipv6 { my $ip = shift; return unless $ip = expand_ipv6($ip); $ip = lc($ip); $ip =~ s/:(0+:){2,}/::/; # flatten multiple groups of 0's to :: # $ip =~ s/(:0+){2,}$/::/; # flatten multiple groups of 0's to :: # $ip =~ s/^0+([1-9a-f])/$1/; # flatten the first segment's leading 0's to a single 0 # $ip =~ s/:0+([1-9a-f])/:$1/g; # flatten each segment, after the first, leading 0's to a single 0 # $ip =~ s/:0+(:)/:0$1/g; # flatten any segments that are just 0's to a single 0 # $ip =~ s/:0+$/:0/g; # flatten the end segment if it's just 0's to a single 0 # $ip =~ s/^0+::/::/; # remove single 0 at the beginning # $ip =~ s/::0+$/::/; # remote single 0 at the end # return $ip; } sub normalize_ip { return !defined $_[0] ? undef : index( $_[0], ':' ) > -1 ? normalize_ipv6( $_[0] ) : normalize_ipv4( $_[0] ); } sub expand_ip { return !defined $_[0] ? undef : index( $_[0], ':' ) > -1 ? expand_ipv6( $_[0] ) : normalize_ipv4( $_[0] ); } 1; } # --- END Cpanel/Validate/IP/Expand.pm { # --- BEGIN Cpanel/IP/Expand.pm package Cpanel::IP::Expand; use strict; use warnings; # use Cpanel::Validate::IP::v4 (); # use Cpanel::Validate::IP::Expand (); sub expand_ip { my ( $ip, $version ) = @_; $ip =~ tr{ \r\n\t}{}d if defined $ip; if ( defined $version && $version eq 6 && Cpanel::Validate::IP::v4::is_valid_ipv4($ip) ) { my @ipv4 = map { $_ + 0 } split /\./, $ip; return "0000:0000:0000:0000:0000:ffff:" . sprintf( '%04x', ( $ipv4[0] << 8 ) + $ipv4[1] ) . ':' . sprintf( '%04x', ( $ipv4[2] << 8 ) + $ipv4[3] ); } my $expanded = Cpanel::Validate::IP::Expand::expand_ip($ip); return $expanded if $expanded; if ( defined $version && $version eq 6 || $ip =~ m/:/ ) { return '0000:0000:0000:0000:0000:0000:0000:0000'; } return '0.0.0.0'; } sub ip2binary_string { my $ip = shift || ''; if ( $ip =~ tr/:// ) { $ip = expand_ip( $ip, 6 ); $ip =~ tr<:><>d; return unpack( 'B128', pack( 'H32', $ip ) ); } return unpack( 'B32', pack( 'C4C4C4C4', split( /\./, $ip ) ) ); } sub first_last_ip_in_range { my ($range) = @_; my ( $range_firstip, $mask ) = split( m{/}, $range ); if ( !length $mask ) { die "Invalid input ($range) -- must be CIDR!"; } my $mask_offset = 0; if ( $range_firstip !~ tr/:// ) { # match as if it were an embedded ipv4 in ipv6 $range_firstip = expand_ip( $range_firstip, 6 ); $mask_offset = ( 128 - 32 ); # If we convert the range from ipv4 to ipv6 we need to move the mask } my $size = 128; my $range_firstip_binary_string = ip2binary_string($range_firstip); my $range_lastip_binary_string = substr( $range_firstip_binary_string, 0, $mask + $mask_offset ) . '1' x ( $size - $mask - $mask_offset ); return ( $range_firstip_binary_string, $range_lastip_binary_string ); } 1; } # --- END Cpanel/IP/Expand.pm { # --- BEGIN Cpanel/Linux/Netlink.pm package Cpanel::Linux::Netlink; use strict; use warnings; use constant DEBUG => 0; # use Cpanel::Exception (); # use Cpanel::Pack (); # use Cpanel::Pack::Template (); my $NETLINK_READ_SIZE = 262144; # Maximum size of netlink message use constant PAGE_SIZE => 0x400; use constant READ_SIZE => 8 * PAGE_SIZE; our $PF_NETLINK = 16; our $AF_INET = 2; our $AF_INET6 = 10; our $NLMSG_NOOP = 0x1; our $NLMSG_ERROR = 0x2; our $NLMSG_DONE = 0x3; our $NLMSG_OVERRUN = 0x4; our $NETLINK_INET_DIAG_26_KERNEL = 0; our $NETLINK_INET_DIAG = 4; our $NLM_F_REQUEST = 1; our $NLM_F_MULTI = 2; # /* Multipart message, terminated by NLMSG_DONE */ our $NLM_F_ROOT = 0x100; our $NLM_F_MATCH = 0x200; # in queries, return all matches our $NLM_F_EXCL = 0x200; # in commands, don't alter if it exists our $NLM_F_CREATE = 0x400; # in commands, create if it does not exist our $NLM_F_ACK = 4; our $SOCK_DGRAM = 2; our $TCPDIAG_GETSOCK = 18; our $INET_DIAG_NOCOOKIE = 0xFFFFFFFF; use constant { PACK_TEMPLATE_U16 => Cpanel::Pack::Template::PACK_TEMPLATE_U16, U16_BYTES_LENGTH => Cpanel::Pack::Template::U16_BYTES_LENGTH, PACK_TEMPLATE_U32 => Cpanel::Pack::Template::PACK_TEMPLATE_U32, U32_BYTES_LENGTH => Cpanel::Pack::Template::U32_BYTES_LENGTH, }; my $NLMSG_HEADER_PACK_OBJ; my $NLMSG_HEADER_PACK_OBJ_SIZE; our @NLMSG_HEADER_TEMPLATE; BEGIN { @NLMSG_HEADER_TEMPLATE = ( 'nlmsg_length' => PACK_TEMPLATE_U32(), #__u32 nlmsg_len; /* Length of message including header. */ 'nlmsg_type' => PACK_TEMPLATE_U16(), #__u16 nlmsg_type; /* Type of message content. */ 'nlmsg_flags' => PACK_TEMPLATE_U16(), #__u16 nlmsg_flags; /* Additional flags. */ 'nlmsg_seq' => PACK_TEMPLATE_U32(), #__u32 nlmsg_seq; /* Sequence number. */ 'nlmsg_pid' => PACK_TEMPLATE_U32(), #__u32 nlmsg_pid; /* Sender port ID. */ ); } my @NETLINK_XACTION_REQUIRED = ( 'message', #hashref, to be sent via “send_pack_obj” 'send_pack_obj', #Cpanel::Pack instance 'recv_pack_obj', #Cpanel::Pack instance 'sock', #Perl socket ); my %_u16_cache; my %_u32_cache; sub netlink_transaction { my (%OPTS) = @_; foreach (@NETLINK_XACTION_REQUIRED) { die "$_ is required for netlink_transaction" if !$OPTS{$_}; } my ( $message_ref, $send_pack_obj, $recv_pack_obj, $sock, $parser, $payload_parser, $header_parms_ar ) = @OPTS{ @NETLINK_XACTION_REQUIRED, 'parser', 'payload_parser', 'header' }; my $packed_nlmsg = _pack_nlmsg_with_header( $send_pack_obj, $message_ref, $header_parms_ar ); if (DEBUG) { require Data::Dumper; print STDERR "[request]:" . Data::Dumper::Dumper($message_ref); } printf STDERR "Send %v02x\n", $packed_nlmsg if DEBUG; send( $sock, $packed_nlmsg, 0 ) or die "send: $!"; my $message_hr; my $packed_response = ''; my $header_pack_size = $NLMSG_HEADER_PACK_OBJ->sizeof(); my $recv_pack_size = $recv_pack_obj->sizeof(); my $msgcount = 0; my ( $msg, $u32, $u16, $nlmsg_length, $nlmsg_type, $nlmsg_flags ); READ_LOOP: while ( !_nlmsg_type_indicates_finished_reading($message_hr) ) { sysread( $sock, $packed_response, $NETLINK_READ_SIZE, length $packed_response ) or die "sysread: $!"; PARSE_LOOP: while (1) { $msg = substr( $packed_response, 0, $header_pack_size, q<> ); $u32 = substr( $msg, 0, U32_BYTES_LENGTH, '' ); $nlmsg_length = $_u32_cache{$u32} //= unpack( PACK_TEMPLATE_U32, $u32 ); $u16 = substr( $msg, 0, U16_BYTES_LENGTH, '' ); $nlmsg_type = $_u16_cache{$u16} //= unpack( PACK_TEMPLATE_U16, $u16 ); $u16 = substr( $msg, 0, U16_BYTES_LENGTH ); $nlmsg_flags = $_u16_cache{$u16} //= unpack( PACK_TEMPLATE_U16, $u16 ); last PARSE_LOOP if !$nlmsg_length || length $packed_response < $nlmsg_length - $NLMSG_HEADER_PACK_OBJ_SIZE; print STDERR "Received message, total size: [$nlmsg_length]\n" if DEBUG; if ( $nlmsg_type == $NLMSG_ERROR ) { require Data::Dumper; my ( $errno, $msg ) = unpack 'i a*', $packed_response; die Cpanel::Exception::create( 'Netlink', [ error => do { local $! = -$errno }, message => $msg ] ); } if ( $recv_pack_size <= length $packed_response ) { my $main_msg = substr( $packed_response, 0, $recv_pack_size, '' ); $message_hr = $recv_pack_obj->unpack_to_hashref($main_msg); if (DEBUG) { require Data::Dumper; printf STDERR "Received %v02x\n", $main_msg; print STDERR "[response]:" . Data::Dumper::Dumper($message_hr); } my $payload = substr( $packed_response, 0, $nlmsg_length - $NLMSG_HEADER_PACK_OBJ_SIZE - $recv_pack_size, q<>, ); if ( $payload_parser && length $payload ) { printf STDERR "payload: Received [%v02x]\n", $payload if DEBUG; $payload_parser->( $msgcount, $message_hr, $payload ); } } last READ_LOOP if _nlmsg_type_flags_indicates_finished_reading( $nlmsg_type, $nlmsg_flags ); $msgcount++; } } $parser->( $msgcount, $message_hr ) if $parser && $nlmsg_type; return 1; } our @INET_DIAG_SOCKID_TEMPLATE = ( 'idiag_sport' => Cpanel::Pack::Template::PACK_TEMPLATE_BE16, #__be16 idiag_sport; 'idiag_dport' => Cpanel::Pack::Template::PACK_TEMPLATE_BE16, #__be16 idiag_dport; 'idiag_src_0' => Cpanel::Pack::Template::PACK_TEMPLATE_BE32, #__be32 idiag_src[0]; 'idiag_src_1' => Cpanel::Pack::Template::PACK_TEMPLATE_BE32, #__be32 idiag_src[1]; 'idiag_src_2' => Cpanel::Pack::Template::PACK_TEMPLATE_BE32, #__be32 idiag_src[2]; 'idiag_src_3' => Cpanel::Pack::Template::PACK_TEMPLATE_BE32, #__be32 idiag_src[3]; 'idiag_dst_0' => Cpanel::Pack::Template::PACK_TEMPLATE_BE32, #__be32 idiag_dst[0]; 'idiag_dst_1' => Cpanel::Pack::Template::PACK_TEMPLATE_BE32, #__be32 idiag_dst[1]; 'idiag_dst_2' => Cpanel::Pack::Template::PACK_TEMPLATE_BE32, #__be32 idiag_dst[2]; 'idiag_dst_3' => Cpanel::Pack::Template::PACK_TEMPLATE_BE32, #__be32 idiag_dst[3]; 'idiag_if' => Cpanel::Pack::Template::PACK_TEMPLATE_U32, #__u32 idiag_if; 'idiag_cookie_0' => Cpanel::Pack::Template::PACK_TEMPLATE_U32, #__u32 idiag_cookie[0]; 'idiag_cookie_1' => Cpanel::Pack::Template::PACK_TEMPLATE_U32, #__u32 idiag_cookie[1]; ); my $INET_DIAG_MSG_PACK_OBJ; our @INET_DIAG_MSG_TEMPLATE = ( 'idiag_family' => Cpanel::Pack::Template::PACK_TEMPLATE_U8, # __u8 idiag_family; /* Family of addresses. */ 'idiag_state' => Cpanel::Pack::Template::PACK_TEMPLATE_U8, # __u8 idiag_state; 'idiag_timer' => Cpanel::Pack::Template::PACK_TEMPLATE_U8, # __u8 idiag_timer; 'idiag_retrans' => Cpanel::Pack::Template::PACK_TEMPLATE_U8, # __u8 idiag_retrans; @INET_DIAG_SOCKID_TEMPLATE, # inet_diag_sockid 'idiag_expires' => Cpanel::Pack::Template::PACK_TEMPLATE_U32, #__u32 idiag_expires; 'idiag_rqueue' => Cpanel::Pack::Template::PACK_TEMPLATE_U32, #__u32 idiag_rqueue; 'idiag_wqueue' => Cpanel::Pack::Template::PACK_TEMPLATE_U32, #__u32 idiag_wqueue; 'idiag_uid' => Cpanel::Pack::Template::PACK_TEMPLATE_U32, #__u32 idiag_uid; 'idiag_inode' => Cpanel::Pack::Template::PACK_TEMPLATE_U32 #__u32 idiag_inode; ); my $INET_DIAG_REQ_PACK_OBJ; our @INET_DIAG_REQ_TEMPLATE = ( 'idiag_family' => Cpanel::Pack::Template::PACK_TEMPLATE_U8, # __u8 idiag_family; /* Family of addresses. */ 'idiag_src_len' => Cpanel::Pack::Template::PACK_TEMPLATE_U8, # __u8 idiag_src_len; 'idiag_dst_len' => Cpanel::Pack::Template::PACK_TEMPLATE_U8, # __u8 idiag_dst_len; 'idiag_ext' => Cpanel::Pack::Template::PACK_TEMPLATE_U8, # __u8 idiag_ext; /* Query extended information */ @INET_DIAG_SOCKID_TEMPLATE, #inet_diag_sockid 'idiag_states' => Cpanel::Pack::Template::PACK_TEMPLATE_U32, #__u32 idiag_states; /* States to dump */ 'idiag_dbs' => Cpanel::Pack::Template::PACK_TEMPLATE_U32 #__u32 idiag_dbs; /* Tables to dump (NI) */ ); sub connection_lookup { my ( $source_address, $source_port, $dest_address, $dest_port ) = @_; die "A source port is required." if !defined $source_port; die "A destination port is required." if !defined $dest_port; my ( $idiag_dst_0, $idiag_dst_1, $idiag_dst_2, $idiag_dst_3 ); my ( $idiag_src_0, $idiag_src_1, $idiag_src_2, $idiag_src_3 ); my ($idiag_family); if ( $dest_address =~ tr/:// ) { require Cpanel::IP::Expand; # hide from exim but not perlcc - not eval quoted ( $idiag_dst_0, $idiag_dst_1, $idiag_dst_2, $idiag_dst_3 ) = unpack( 'N4', pack( 'n8', split /:/, Cpanel::IP::Expand::expand_ip($dest_address) ) ); ( $idiag_src_0, $idiag_src_1, $idiag_src_2, $idiag_src_3 ) = unpack( 'N4', pack( 'n8', split /:/, Cpanel::IP::Expand::expand_ip($source_address) ) ); $idiag_family = $AF_INET6; } else { my $u32_dest_address = unpack( 'N', pack( 'C4', split( /\D/, $dest_address, 4 ) ) ); my $u32_source_address = unpack( 'N', pack( 'C4', split( /\D/, $source_address, 4 ) ) ); $idiag_src_0 = $u32_source_address; $idiag_dst_0 = $u32_dest_address; $idiag_family = $AF_INET; } my $sock; socket( $sock, $PF_NETLINK, $SOCK_DGRAM, $NETLINK_INET_DIAG ) or die "socket: $!"; $INET_DIAG_REQ_PACK_OBJ ||= Cpanel::Pack->new( \@INET_DIAG_REQ_TEMPLATE ); $INET_DIAG_MSG_PACK_OBJ ||= Cpanel::Pack->new( \@INET_DIAG_MSG_TEMPLATE ); my %RESPONSE; netlink_transaction( 'message' => { 'idiag_family' => $idiag_family, 'idiag_dst_0' => $idiag_dst_0, 'idiag_dst_1' => $idiag_dst_1, 'idiag_dst_2' => $idiag_dst_2, 'idiag_dst_3' => $idiag_dst_3, 'idiag_dport' => $dest_port, 'idiag_src_0' => $idiag_src_0, 'idiag_src_1' => $idiag_src_1, 'idiag_src_2' => $idiag_src_2, 'idiag_src_3' => $idiag_src_3, 'idiag_sport' => $source_port, 'idiag_cookie_0' => $INET_DIAG_NOCOOKIE, 'idiag_cookie_1' => $INET_DIAG_NOCOOKIE, }, 'sock' => $sock, 'send_pack_obj' => $INET_DIAG_REQ_PACK_OBJ, 'recv_pack_obj' => $INET_DIAG_MSG_PACK_OBJ, 'parser' => sub { my ( undef, $response_ref ) = @_; %RESPONSE = %$response_ref if ( $response_ref && 'HASH' eq ref $response_ref ); } ); return \%RESPONSE; } my @NETLINK_SEND_HEADER = ( 'nlmsg_length' => undef, #gets put in place 'nlmsg_type' => $TCPDIAG_GETSOCK, 'nlmsg_flags' => 0, #gets |=’d with $NLM_F_REQUEST 'nlmsg_pid' => undef, #gets put in place 'nlmsg_seq' => 2, #default ); sub _pack_nlmsg_with_header { my ( $send_pack_obj, $message_ref, $header_parms_ar ) = @_; my $nlmsg = $send_pack_obj->pack_from_hashref($message_ref); if ( !$NLMSG_HEADER_PACK_OBJ ) { $NLMSG_HEADER_PACK_OBJ = Cpanel::Pack->new( \@NLMSG_HEADER_TEMPLATE ); $NLMSG_HEADER_PACK_OBJ_SIZE = $NLMSG_HEADER_PACK_OBJ->sizeof(); } my %header_data = ( @NETLINK_SEND_HEADER, ( $header_parms_ar ? @$header_parms_ar : () ), nlmsg_length => $NLMSG_HEADER_PACK_OBJ_SIZE + length $nlmsg, nlmsg_pid => $$, ); $header_data{'nlmsg_flags'} |= $NLM_F_REQUEST; my $hdr_str = $NLMSG_HEADER_PACK_OBJ->pack_from_hashref( \%header_data ); return $hdr_str . $nlmsg; } sub _nlmsg_type_indicates_finished_reading { return _nlmsg_type_flags_indicates_finished_reading( $_[0]->{'nlmsg_type'}, $_[0]->{'nlmsg_flags'} ); } sub _nlmsg_type_flags_indicates_finished_reading { return 0 if !length $_[0]; return ( $_[0] == $NLMSG_ERROR || ( $_[1] & $NLM_F_MULTI && $_[0] == $NLMSG_DONE ) || !( $_[1] & $NLM_F_MULTI ) ) ? 1 : 0; } sub expect_acknowledgment { my ( $my_sysread, $socket, $sequence ) = @_; my $NETLINK_HEADER = Cpanel::Pack->new( \@NLMSG_HEADER_TEMPLATE ); my $response_buffer = ''; my $header_hr; my $error_code; do { while ( length $response_buffer < $NETLINK_HEADER->sizeof() ) { $my_sysread->( $socket, \$response_buffer, READ_SIZE(), length $response_buffer ) or return "sysread, message header: $!"; } $header_hr = $NETLINK_HEADER->unpack_to_hashref( substr( $response_buffer, 0, $NETLINK_HEADER->sizeof() ) ); while ( length $response_buffer < $header_hr->{nlmsg_length} ) { $my_sysread->( $socket, \$response_buffer, READ_SIZE(), length $response_buffer ) or return "sysread, message body: $!"; } my $message = substr( $response_buffer, 0, $header_hr->{nlmsg_length}, '' ); $error_code = 0; if ( $header_hr->{nlmsg_type} == $NLMSG_ERROR ) { $error_code = unpack( Cpanel::Pack::Template::PACK_TEMPLATE_U32, substr( $message, $NETLINK_HEADER->sizeof(), Cpanel::Pack::Template::U32_BYTES_LENGTH ) ); } if ( $header_hr->{nlmsg_seq} eq $sequence ) { if ( $header_hr->{nlmsg_type} == $NLMSG_ERROR && $error_code != 0 ) { local $! = -$error_code; return "Received error code when expecting acknowledgement: $!\n"; } if ( $header_hr->{nlmsg_type} == $NLMSG_OVERRUN ) { return "Data lost due to message overrun"; } if ( $header_hr->{nlmsg_type} == $NLMSG_DONE ) { return "Received multipart data when expecting ACK"; } } } while ( $header_hr->{nlmsg_seq} ne $sequence || $header_hr->{nlmsg_type} != $NLMSG_ERROR || $error_code != 0 ); return undef; } 1; } # --- END Cpanel/Linux/Netlink.pm { # --- BEGIN Cpanel/Linux/Proc/Net/Tcp.pm package Cpanel::Linux::Proc::Net::Tcp; use strict; our $PROC_NET_TCP = '/proc/net/tcp'; our $PROC_NET_TCP6 = '/proc/net/tcp6'; sub connection_lookup { my ( $remote_address, $remote_port, $local_address, $local_port ) = @_; my ( $tcp_file, $remote_ltl_endian_hex_address, $remote_hex_port, $local_ltl_endian_hex_address, $local_hex_port ); $remote_hex_port = _dec_port_to_hex_port($remote_port); $local_hex_port = _dec_port_to_hex_port($local_port); if ( $remote_address =~ tr/:// ) { #ipv6 $tcp_file = $PROC_NET_TCP6; $remote_ltl_endian_hex_address = _ipv6_text_to_little_endian_hex_address($remote_address); $local_ltl_endian_hex_address = _ipv6_text_to_little_endian_hex_address($local_address); } else { $tcp_file = $PROC_NET_TCP; $remote_ltl_endian_hex_address = _ipv4_txt_to_little_endian_hex_address($remote_address); $local_ltl_endian_hex_address = _ipv4_txt_to_little_endian_hex_address($local_address); } if ( open( my $tcp_fh, '<', $tcp_file ) ) { my $uid; while ( readline($tcp_fh) ) { if ( m/^\s*\d+:\s+([\dA-F]{8}(?:[\dA-F]{24})?):([\dA-F]{4})\s+([\dA-F]{8}(?:[\dA-F]{24})?):([\dA-F]{4})\s+(\S+)\s+\S+\s+\S+\s+\S+\s+(\d+)/ && $remote_ltl_endian_hex_address eq $1 && $remote_hex_port eq $2 && $local_ltl_endian_hex_address eq $3 && $local_hex_port eq $4 ) { $uid = $6; last; } } return $uid; } return; } sub _dec_port_to_hex_port { my ($dec_port) = @_; return sprintf( '%04X', $dec_port ); } sub _ipv4_txt_to_little_endian_hex_address { my ($ipv4_txt) = @_; return sprintf( "%08X", unpack( 'V', pack( 'C4', split( /\D/, $ipv4_txt, 4 ) ) ) ); } sub _ipv6_text_to_little_endian_hex_address { my ($ipv6_txt) = @_; require Cpanel::IP::Expand; # hide from exim but not perlcc - not eval quoted my $hexip = ''; my @ip = split /:/, Cpanel::IP::Expand::expand_ip( $ipv6_txt, 6 ); while (@ip) { my $block1 = shift @ip; my $block2 = shift @ip; $hexip .= uc substr( $block2, 2, 2 ) . uc substr( $block2, 0, 2 ) . uc substr( $block1, 2, 2 ) . uc substr( $block1, 0, 2 ); } return $hexip; } 1; } # --- END Cpanel/Linux/Proc/Net/Tcp.pm { # --- BEGIN Cpanel/Ident.pm package Cpanel::Ident; use strict; our $TESTING_FLAGS = 0; # FOR TESTING our $USE_NETLINK = 1; # FOR TESTING our $USE_PROC = 2; # FOR TESTING use constant NOTFOUND => 0xff_ff_ff_ff; sub identify_local_connection { my ( $source_address, $source_port, $dest_address, $dest_port ) = @_; if ( !defined($source_port) || !defined($dest_port) ) { die 'Need source and destination ports!'; } my $netlink_failed; if ( !$TESTING_FLAGS || $TESTING_FLAGS == $USE_NETLINK ) { require Cpanel::Linux::Netlink; # hide from exim but not perlcc - not eval quoted my $response; local $@; eval { $response = Cpanel::Linux::Netlink::connection_lookup( $source_address, $source_port, $dest_address, $dest_port, ); }; if ($@) { $netlink_failed = 1; warn; } elsif ($response && defined $response->{'idiag_state'} && ( $response->{'idiag_state'} != 1 && $response->{'idiag_state'} != 8 && $response->{'idiag_state'} != 10 ) ) { return -1; } elsif ($response && ref $response && $response->{'idiag_dport'} && defined( $response->{'idiag_uid'} ) && $response->{'idiag_uid'} != NOTFOUND() ) { return $response->{'idiag_uid'}; } } if ( $netlink_failed || $TESTING_FLAGS == $USE_PROC ) { require Cpanel::Linux::Proc::Net::Tcp; # hide from exim but not perlcc - not eval quoted my $uid = Cpanel::Linux::Proc::Net::Tcp::connection_lookup( $source_address, $source_port, $dest_address, $dest_port ); return $uid if defined $uid; } return; } 1; } # --- END Cpanel/Ident.pm { # --- BEGIN Cpanel/Autodie.pm package Cpanel::Autodie; use strict; use warnings; sub _ENOENT { return 2; } sub _EEXIST { return 17; } sub _EINTR { return 4; } sub import { shift; _load_function($_) for @_; return; } our $AUTOLOAD; sub AUTOLOAD { substr( $AUTOLOAD, 0, 1 + rindex( $AUTOLOAD, ':' ) ) = q<>; _load_function($AUTOLOAD); goto &{ Cpanel::Autodie->can($AUTOLOAD) }; } sub _load_function { _require("Cpanel/Autodie/CORE/$_[0].pm"); return; } sub _require { local ( $!, $^E, $@ ); require $_[0]; return; } 1; } # --- END Cpanel/Autodie.pm { # --- BEGIN Cpanel/Autodie/CORE/exists.pm package Cpanel::Autodie; use strict; use warnings; sub exists { ## no critic qw( RequireArgUnpacking ) local ( $!, $^E ); if ( ${^GLOBAL_PHASE} eq 'START' ) { _die_err( $_[0], "do not access the filesystem at compile time" ); } return 1 if -e $_[0]; return 0 if $! == _ENOENT(); return _die_err( $_[0], $! ); } sub exists_nofollow { my ($path) = @_; local ( $!, $^E ); return 1 if CORE::lstat $path; return 0 if $! == _ENOENT(); return _die_err( $path, $! ); } sub _die_err { my ( $path, $err ) = @_; local $@; # $! is already local()ed. require Cpanel::Exception; die Cpanel::Exception::create( 'IO::StatError', [ error => $err, path => $path ] ); } 1; } # --- END Cpanel/Autodie/CORE/exists.pm { # --- BEGIN Cpanel/Autodie/CORE/exists_nofollow.pm package Cpanel::Autodie; use strict; use warnings; # use Cpanel::Autodie::CORE::exists(); # PPI NO PARSE 1; } # --- END Cpanel/Autodie/CORE/exists_nofollow.pm { # --- BEGIN Cpanel/Autodie/More/Lite.pm package Cpanel::Autodie::More::Lite; use strict; use warnings; # use Cpanel::Autodie (); # use Cpanel::Autodie::CORE::exists (); # PPI USE OK - reload so we can map the symbol below # use Cpanel::Autodie::CORE::exists_nofollow (); # PPI USE OK - reload so we can map the symbol below BEGIN { *exists = *Cpanel::Autodie::exists; *exists_nofollow = *Cpanel::Autodie::exists_nofollow; } 1; } # --- END Cpanel/Autodie/More/Lite.pm { # --- BEGIN Cpanel/Services/Enabled/Spamd.pm package Cpanel::Services::Enabled::Spamd; use strict; use warnings; # use Cpanel::Autodie::More::Lite (); our $_TOUCHFILE_PATH = '/etc/spamddisable'; sub is_enabled { return !Cpanel::Autodie::More::Lite::exists($_TOUCHFILE_PATH); } 1; } # --- END Cpanel/Services/Enabled/Spamd.pm { # --- BEGIN Cpanel/FileUtils/Dir.pm package Cpanel::FileUtils::Dir; use strict; use warnings; # use Cpanel::Exception (); use constant _ENOENT => 2; sub directory_has_nodes { return directory_has_nodes_if_exists( $_[0] ) // do { local $! = _ENOENT(); die _opendir_err( $_[0] ); }; } sub directory_has_nodes_if_exists { my ($dir) = @_; local $!; opendir my $dh, $dir or do { if ( $! == _ENOENT() ) { return undef; } die _opendir_err($dir); }; local $!; my $has_nodes = 0; while ( my $node = readdir $dh ) { next if $node eq '.' || $node eq '..'; $has_nodes = 1; last; } _check_for_readdir_error($dir) if !$has_nodes; _closedir( $dh, $dir ); return $has_nodes; } sub get_directory_nodes_if_exists { my ($dir) = @_; local $!; if ( opendir my $dh, $dir ) { return _read_directory_nodes( $dh, $dir ); } elsif ( $! != _ENOENT() ) { die _opendir_err($dir); } return undef; } sub get_directory_nodes { return _read_directory_nodes( _opendir( $_[0] ), $_[0] ); } sub _read_directory_nodes { ## no critic qw(Subroutines::RequireArgUnpacking) -- used in loops local $!; my @nodes = grep { $_ ne '.' && $_ ne '..' } readdir( $_[0] ); _check_for_readdir_error( $_[0] ); _closedir( $_[0], $_[1] ); return \@nodes; } sub _check_for_readdir_error { if ( $! && ( $^V >= v5.20.0 ) ) { die Cpanel::Exception::create( 'IO::DirectoryReadError', [ path => $_[0], error => $! ] ); } return; } sub _opendir { local $!; opendir my $dh, $_[0] or do { die _opendir_err( $_[0] ); }; return $dh; } sub _closedir { local $!; closedir $_[0] or do { die Cpanel::Exception::create( 'IO::DirectoryCloseError', [ path => $_[1], error => $! ] ); }; return; } sub _opendir_err { return Cpanel::Exception::create( 'IO::DirectoryOpenError', [ path => $_[0], error => $! ] ); } 1; } # --- END Cpanel/FileUtils/Dir.pm { # --- BEGIN Cpanel/DKIM/ValidityCache.pm package Cpanel::DKIM::ValidityCache; use strict; use warnings; # use Cpanel::Autodie (); our $BASE_DIRECTORY = '/var/cpanel/domain_keys/validity_cache'; sub _BASE { return $BASE_DIRECTORY; } sub get { my ( undef, $entry ) = @_; return Cpanel::Autodie::exists("$BASE_DIRECTORY/$entry"); } sub get_all { require Cpanel::FileUtils::Dir; return Cpanel::FileUtils::Dir::get_directory_nodes_if_exists($BASE_DIRECTORY); } 1; } # --- END Cpanel/DKIM/ValidityCache.pm { # --- BEGIN Cpanel/Context.pm package Cpanel::Context; use strict; use warnings; # use Cpanel::Exception (); sub must_be_list { return 1 if ( caller(1) )[5]; # 5 = wantarray my $msg = ( caller(1) )[3]; # 3 = subroutine $msg .= $_[0] if defined $_[0]; return _die_context( 'list', $msg ); } sub must_not_be_scalar { my ($message) = @_; my $wa = ( caller(1) )[5]; # 5 = wantarray if ( !$wa && defined $wa ) { _die_context( 'list or void', $message ); } return 1; } sub must_not_be_void { return if defined( ( caller 1 )[5] ); return _die_context('scalar or list'); } sub _die_context { my ( $context, $message ) = @_; local $Carp::CarpInternal{__PACKAGE__} if $INC{'Carp.pm'}; my $to_throw = length $message ? "Must be $context context ($message)!" : "Must be $context context!"; die Cpanel::Exception::create_raw( 'ContextError', $to_throw ); } 1; } # --- END Cpanel/Context.pm { # --- BEGIN Cpanel/ProcessInfo.pm package Cpanel::ProcessInfo; use strict; use warnings; # use Cpanel::Context (); # use Cpanel::Autodie (); our $VERSION = '1.0'; sub get_pid_lineage { Cpanel::Context::must_be_list(); my @lineage; my $ppid = getppid(); while ( $ppid > 1 ) { push @lineage, $ppid; $ppid = get_parent_pid($ppid); } return @lineage; } sub get_parent_pid { _die_if_pid_invalid( $_[0] ); return getppid() if $_[0] == $$; if ( open( my $proc_status_fh, '<', "/proc/$_[0]/status" ) ) { local $/; my %status = map { lc $_->[0] => $_->[1] } map { [ ( split( /\s*:\s*/, $_ ) )[ 0, 1 ] ] } grep { index( $_, ':' ) > -1 } split( /\n/, readline($proc_status_fh) ); return $status{'ppid'}; } return undef; } sub get_pid_exe { _die_if_pid_invalid( $_[0] ); return Cpanel::Autodie::readlink_if_exists( '/proc/' . $_[0] . '/exe' ); } sub get_pid_cmdline { _die_if_pid_invalid( $_[0] ); if ( open( my $cmdline, '<', "/proc/$_[0]/cmdline" ) ) { local $/; my $cmdline = readline($cmdline); $cmdline =~ tr{\0}{ }; $cmdline =~ tr{\r\n}{}d; substr( $cmdline, -1, 1, '' ) if substr( $cmdline, -1 ) eq ' '; return $cmdline; } return ''; } sub get_pid_cwd { _die_if_pid_invalid( $_[0] ); return readlink( '/proc/' . $_[0] . '/cwd' ) || '/'; } sub _die_if_pid_invalid { die "Invalid PID: $_[0]" if !length $_[0] || $_[0] =~ tr{0-9}{}c; return; } 1; } # --- END Cpanel/ProcessInfo.pm { # --- BEGIN Cpanel/Fcntl/Constants.pm package Cpanel::Fcntl::Constants; use strict; use warnings; BEGIN { our $O_RDONLY = 0; our $O_WRONLY = 1; our $O_RDWR = 2; our $O_ACCMODE = 3; our $F_GETFD = 1; our $F_SETFD = 2; our $F_GETFL = 3; our $F_SETFL = 4; our $SEEK_SET = 0; our $SEEK_CUR = 1; our $SEEK_END = 2; our $S_IWOTH = 2; our $S_ISUID = 2048; our $S_ISGID = 1024; our $O_CREAT = 64; our $O_EXCL = 128; our $O_TRUNC = 512; our $O_APPEND = 1024; our $O_NONBLOCK = 2048; our $O_DIRECTORY = 65536; our $O_NOFOLLOW = 131072; our $O_CLOEXEC = 524288; our $S_IFREG = 32768; our $S_IFDIR = 16384; our $S_IFCHR = 8192; our $S_IFBLK = 24576; our $S_IFIFO = 4096; our $S_IFLNK = 40960; our $S_IFSOCK = 49152; our $S_IFMT = 61440; our $LOCK_SH = 1; our $LOCK_EX = 2; our $LOCK_NB = 4; our $LOCK_UN = 8; our $FD_CLOEXEC = 1; } 1; } # --- END Cpanel/Fcntl/Constants.pm { # --- BEGIN Cpanel/Socket/Constants.pm package Cpanel::Socket::Constants; use strict; use warnings; our $SO_REUSEADDR = 2; our $AF_UNIX = 1; our $AF_INET = 2; our $PF_INET = 2; our $AF_INET6 = 10; our $PF_INET6 = 10; our $PROTO_IP = 0; our $PROTO_ICMP = 1; our $PROTO_TCP = 6; our $PROTO_UDP = 17; our $IPPROTO_TCP; *IPPROTO_TCP = \$PROTO_TCP; our $SO_PEERCRED = 17; our $SOL_SOCKET = 1; our $SOCK_STREAM = 1; our $SOCK_NONBLOCK = 2048; our $SHUT_RD = 0; our $SHUT_WR = 1; our $SHUT_RDWR = 2; our $MSG_PEEK = 2; our $MSG_NOSIGNAL = 16384; 1; } # --- END Cpanel/Socket/Constants.pm { # --- BEGIN Cpanel/Hulk/Constants.pm package Cpanel::Hulk::Constants; use strict; # use Cpanel::Fcntl::Constants (); # use Cpanel::Socket::Constants (); *F_GETFL = \$Cpanel::Fcntl::Constants::F_GETFL; *F_SETFL = \$Cpanel::Fcntl::Constants::F_SETFL; *O_NONBLOCK = \$Cpanel::Fcntl::Constants::O_NONBLOCK; our $EINTR = 4; our $EPIPE = 32; our $EINPROGRESS = 115; our $ETIMEDOUT = 110; our $EISCONN = 106; our $ECONNRESET = 104; our $EAGAIN = 11; *PROTO_IP = \$Cpanel::Socket::Constants::PROTO_IP; *PROTO_ICMP = \$Cpanel::Socket::Constants::PROTO_ICMP; *PROTO_TCP = \$Cpanel::Socket::Constants::PROTO_TCP; *SO_PEERCRED = \$Cpanel::Socket::Constants::SO_PEERCRED; *SOL_SOCKET = \$Cpanel::Socket::Constants::SOL_SOCKET; *SOCK_STREAM = \$Cpanel::Socket::Constants::SOCK_STREAM; *AF_INET6 = \$Cpanel::Socket::Constants::AF_INET6; *AF_INET = \$Cpanel::Socket::Constants::AF_INET; *AF_UNIX = \$Cpanel::Socket::Constants::AF_UNIX; our $TOKEN_SALT_BASE = '$6$'; our $SALT_LENGTH = 16; our $TIME_BASE = 1410000000; our $SIX_HOURS_IN_SECONDS = 21600; 1; } # --- END Cpanel/Hulk/Constants.pm { # --- BEGIN Cpanel/ApacheServerStatus.pm package Cpanel::ApacheServerStatus; # use Cpanel::Hulk::Constants (); sub new { my ($class) = @_; my $obj = {}; bless $obj, $class; my $html = $obj->fetch_server_status_html(); $html =~ m/<table[^\>]*>(.*?)<\/table[^\>]*>/is; my $inner_table = $1; $inner_table =~ s/[\r\n\0]//g; my $line_count = 0; my ( @index, @data, %server_status ); while ( $inner_table =~ m/<tr[^\>]*>(.*?)<\/tr[^\>]*>/isg ) { my $contents = $1; @data = map { s/^\s+//; s/\s+$//; lc $_; } ( $contents =~ m/(?:<[^\>]+>)+([^\<]+)/isg ); if ( $line_count == 0 ) { @index = @data; } else { my $count = 0; my %named_data = map { $index[ $count++ ] => $_; } @data; $server_status{ $named_data{'pid'} } = \%named_data; } $line_count++; } $obj->{'server_status'} = \%server_status; return $obj; } sub get_status_by_pid { my ( $self, $pid ) = @_; return $self->{'server_status'}->{$pid}; } sub get_apache_port { if ( open( my $ap_port_fh, '<', '/var/cpanel/config/apache/port' ) ) { my $port_txt = readline($ap_port_fh); chomp($port_txt); if ( $port_txt =~ m/:/ ) { return ( split( m/:/, $port_txt ) )[1]; } elsif ( $port_txt =~ /^[0-9]+$/ ) { return $port_txt; } } } sub fetch_server_status_html { my ($self) = @_; my $port = 80; my $html; eval { my $socket_scc; if ( !socket( $socket_scc, $Cpanel::Hulk::Constants::AF_INET, $Cpanel::Hulk::Constants::SOCK_STREAM, $Cpanel::Hulk::Constants::PROTO_TCP ) || !$socket_scc ) { die "Could not setup tcp socket for connection to $port: $!"; } if ( !connect( $socket_scc, pack( 'S n a4 x8', $Cpanel::Hulk::Constants::AF_INET, $port, ( pack 'C4', ( split /\./, "127.0.0.1" ) ) ) ) ) { my $non_default_port = $self->get_apache_port(); if ( $non_default_port && $non_default_port != $port ) { if ( !connect( $socket_scc, pack( 'S n a4 x8', $Cpanel::Hulk::Constants::AF_INET, $non_default_port, ( pack 'C4', ( split /\./, "127.0.0.1" ) ) ) ) ) { die "Unable to connect to port $non_default_port on 127.0.0.1: $!"; } } } syswrite( $socket_scc, "GET /whm-server-status HTTP/1.0\r\nHost: localhost\r\nConnection: close\r\n\r\n" ); local $/; $html = readline($socket_scc); close($socket_scc); }; $html; } 1; } # --- END Cpanel/ApacheServerStatus.pm { # --- BEGIN Cpanel/Server/Type.pm package Cpanel::Server::Type; use cPstrict; use constant NUMBER_OF_USERS_TO_ASSUME_IF_UNREADABLE => 1; sub _get_license_file_path { return q{/usr/local/cpanel/cpanel.lisc} } sub _get_dnsonly_file_path { return q{/var/cpanel/dnsonly} } use constant _ENOENT => 2; my @server_config; our %PRODUCTS; our $MAXUSERS; our %FIELDS; our ( $DNSONLY_MODE, $NODE_MODE ); sub is_dnsonly { return $DNSONLY_MODE if defined $DNSONLY_MODE; return 1 if -e _get_dnsonly_file_path(); return 0 if $! == _ENOENT(); my $err = $!; if ( _read_license() ) { return $PRODUCTS{'dnsonly'} ? 1 : 0; } die sprintf( 'stat(%s): %s', _get_dnsonly_file_path(), "$err" ); } sub get_producttype { return $NODE_MODE if defined $NODE_MODE; return 'DNSONLY' unless _read_license(); return 'STANDARD' if $PRODUCTS{'cpanel'}; foreach my $product (qw/dnsnode mailnode databasenode dnsonly/) { return uc($product) if $PRODUCTS{$product}; } return 'DNSONLY'; } sub get_max_users { return $MAXUSERS if defined $MAXUSERS; return NUMBER_OF_USERS_TO_ASSUME_IF_UNREADABLE unless _read_license(); return $MAXUSERS // NUMBER_OF_USERS_TO_ASSUME_IF_UNREADABLE; } sub has_els { return $FIELDS{els} if defined $FIELDS{els}; return 0 unless _read_license(); return $FIELDS{els} // 0; } sub get_license_expire_gmt_date { return $FIELDS{'license_expire_gmt_date'} if defined $FIELDS{'license_expire_gmt_date'}; return 0 unless _read_license(); return $FIELDS{'license_expire_gmt_date'} // 0; } sub is_licensed_for_product ($product) { return unless $product; $product = lc $product; return unless _read_license(); return exists $PRODUCTS{$product}; } sub get_features { return unless _read_license(); my @features = split( ",", $FIELDS{'features'} // '' ); return @features; } sub has_feature ( $feature = undef ) { length $feature or return; return ( grep { $_ eq $feature } get_features() ) ? 1 : 0; } sub get_products { return unless _read_license(); return keys %PRODUCTS; } sub _read_license { my $LICENSE_FILE = _get_license_file_path(); my @new_stat = stat($LICENSE_FILE) if @server_config; if ( @server_config && @new_stat && $new_stat[9] == $server_config[9] && $new_stat[7] == $server_config[7] ) { return 1; } open( my $fh, '<', $LICENSE_FILE ) or do { if ( $! != _ENOENT() ) { warn "open($LICENSE_FILE): $!"; } return; }; _reset_cache(); my $content; read( $fh, $content, 1024 ) // do { warn "read($LICENSE_FILE): $!"; $content = q<>; }; return _parse_license_contents_sr( $fh, \$content ); } sub _parse_license_contents_to_hashref ($content_sr) { my %vals = map { ( split( m{: }, $_ ) )[ 0, 1 ] } split( m{\n}, $$content_sr ); return \%vals; } sub _parse_license_contents_sr ( $fh, $content_sr ) { my $vals_hr = _parse_license_contents_to_hashref($content_sr); if ( length $vals_hr->{'products'} ) { %PRODUCTS = map { ( $_ => 1 ) } split( ",", $vals_hr->{'products'} ); } else { return; } if ( length $vals_hr->{'maxusers'} ) { $MAXUSERS //= int $vals_hr->{'maxusers'}; } else { return; } foreach my $field (qw/license_expire_time license_expire_gmt_date support_expire_time updates_expire_time/) { $FIELDS{$field} = $vals_hr->{$field} // 0; } foreach my $field (qw/client features/) { $FIELDS{$field} = $vals_hr->{$field} // ''; } if ( length $vals_hr->{'fields'} ) { foreach my $field ( split( ",", $vals_hr->{'fields'} ) ) { my ( $k, $v ) = split( '=', $field, 2 ); $FIELDS{$k} = $v; } } else { return; } @server_config = stat($fh); return 1; } sub _reset_cache { undef %PRODUCTS; undef %FIELDS; undef @server_config; undef $MAXUSERS; undef $DNSONLY_MODE; return; } 1; } # --- END Cpanel/Server/Type.pm { # --- BEGIN Cpanel/Server/Type/Profile/Constants.pm package Cpanel::Server::Type::Profile::Constants; use strict; use warnings; use constant { DNSNODE => "DNSNODE", DATABASENODE => "DATABASENODE", DNSONLY => "DNSONLY", MAILNODE => "MAILNODE", STANDARD => "STANDARD" }; our %PROFILE_CHILD_WORKLOADS = ( MAILNODE() => ['Mail'], ); 1; } # --- END Cpanel/Server/Type/Profile/Constants.pm { # --- BEGIN Cpanel/LoadModule.pm package Cpanel::LoadModule; use strict; # use Cpanel::Exception (); # use Cpanel::LoadModule::Utils (); my $logger; my $has_perl_dir = 0; sub _logger_warn { my ( $msg, $fail_ok ) = @_; return if $fail_ok && $ENV{'CPANEL_BASE_INSTALL'} && index( $^X, '/usr/local/cpanel' ) == -1; if ( $INC{'Cpanel/Logger.pm'} ) { $logger ||= 'Cpanel::Logger'->new(); $logger->warn($msg); } return warn $msg; } sub _reset_has_perl_dir { $has_perl_dir = 0; return; } sub load_perl_module { ## no critic qw(Subroutines::RequireArgUnpacking) if ( -1 != index( $_[0], q<'> ) ) { die Cpanel::Exception::create_raw( 'InvalidParameter', "Module names with single-quotes are prohibited. ($_[0])" ); } return $_[0] if Cpanel::LoadModule::Utils::module_is_loaded( $_[0] ); my ( $mod, @LIST ) = @_; local ( $!, $@ ); if ( !is_valid_module_name($mod) ) { die Cpanel::Exception::create( 'InvalidParameter', '“[_1]” is not a valid name for a Perl module.', [$mod] ); } my $args_str; if (@LIST) { $args_str = join ',', map { die "Only scalar arguments allowed in LIST! (@LIST)" if ref; _single_quote($_); } @LIST; } else { $args_str = q<>; } eval "use $mod ($args_str);"; ## no critic qw(BuiltinFunctions::ProhibitStringyEval) if ($@) { die Cpanel::Exception::create( 'ModuleLoadError', [ module => $mod, error => $@ ] ); } return $mod; } *module_is_loaded = *Cpanel::LoadModule::Utils::module_is_loaded; *is_valid_module_name = *Cpanel::LoadModule::Utils::is_valid_module_name; sub loadmodule { return 1 if cpanel_namespace_module_is_loaded( $_[0] ); return _modloader( $_[0] ); } sub lazy_load_module { my $mod = shift; my $mod_path = $mod; $mod_path =~ s{::}{/}g; if ( exists $INC{ $mod_path . '.pm' } ) { return; } if ( !is_valid_module_name($mod) ) { _logger_warn("Cpanel::LoadModule: Invalid module name ($mod)"); return; } eval "use $mod ();"; if ($@) { delete $INC{ $mod_path . '.pm' }; _logger_warn( "Cpanel::LoadModule:: Failed to load module $mod - $@", 1 ); return; } return 1; } sub cpanel_namespace_module_is_loaded { my ($modpart) = @_; $modpart =~ s{::}{/}g; return exists $INC{"Cpanel/$modpart.pm"} ? 1 : 0; } sub _modloader { my $module = shift; if ( !$module ) { _logger_warn("Empty module name passed to modloader"); return; } if ( !is_valid_module_name($module) ) { _logger_warn("Invalid module name ($module) passed to modloader"); return; } eval qq[ use Cpanel::${module}; Cpanel::${module}::${module}_init() if "Cpanel::${module}"->can("${module}_init"); ]; # PPI USE OK - This looks like usage of the Cpanel module and it's not. if ($@) { _logger_warn("Error loading module $module - $@"); return; } return 1; } sub _single_quote { local ($_) = $_[0]; s/([\\'])/\\$1/g; return qq('$_'); } 1; } # --- END Cpanel/LoadModule.pm { # --- BEGIN Cpanel/Server/Type/Profile.pm package Cpanel::Server::Type::Profile; use strict; use warnings; # use Cpanel::Server::Type (); # use Cpanel::Server::Type::Profile::Constants (); our %ENABLED_IN_ALL_ROLES = ( 'Cpanel::Server::Type::Role::MailSend' => 1, 'Cpanel::Server::Type::Role::MailLocal' => 1, ); our %_META = ( STANDARD => { experimental => 0, enabled_roles => [ qw( Cpanel::Server::Type::Role::CalendarContact Cpanel::Server::Type::Role::DNS Cpanel::Server::Type::Role::FTP Cpanel::Server::Type::Role::FileStorage Cpanel::Server::Type::Role::MailReceive Cpanel::Server::Type::Role::MailRelay Cpanel::Server::Type::Role::MySQL Cpanel::Server::Type::Role::Postgres Cpanel::Server::Type::Role::SpamFilter Cpanel::Server::Type::Role::Webmail Cpanel::Server::Type::Role::WebDisk Cpanel::Server::Type::Role::WebServer ), keys %ENABLED_IN_ALL_ROLES ] }, MAILNODE => { experimental => 0, enabled_roles => [ qw( Cpanel::Server::Type::Role::CalendarContact Cpanel::Server::Type::Role::MailReceive Cpanel::Server::Type::Role::MailRelay Cpanel::Server::Type::Role::Webmail ), keys %ENABLED_IN_ALL_ROLES ], optional_roles => [ qw( Cpanel::Server::Type::Role::MySQL Cpanel::Server::Type::Role::Postgres Cpanel::Server::Type::Role::DNS Cpanel::Server::Type::Role::SpamFilter ) ] }, DNSNODE => { experimental => 0, enabled_roles => [ qw( Cpanel::Server::Type::Role::DNS ), keys %ENABLED_IN_ALL_ROLES ], optional_roles => [ qw( Cpanel::Server::Type::Role::MySQL Cpanel::Server::Type::Role::MailRelay ) ], }, DATABASENODE => { experimental => 1, enabled_roles => [ qw( Cpanel::Server::Type::Role::MySQL ), keys %ENABLED_IN_ALL_ROLES ], optional_roles => [ qw( Cpanel::Server::Type::Role::Postgres ) ] } ); our ( $DNSNODE_MODE, $MAILNODE_MODE, $DATABASENODE_MODE ); my $_CURRENT_PROFILE; sub get_current_profile { return $_CURRENT_PROFILE if defined $_CURRENT_PROFILE; my $product_type = Cpanel::Server::Type::get_producttype(); if ( $product_type && $product_type ne Cpanel::Server::Type::Profile::Constants::STANDARD() ) { return $_CURRENT_PROFILE = $product_type; } my $roles = {}; require Cpanel::LoadModule; PROFILE: foreach my $profile ( keys %_META ) { next if $profile eq Cpanel::Server::Type::Profile::Constants::STANDARD(); my $disabled_roles_ar = get_disabled_roles_for_profile($profile); if ($disabled_roles_ar) { foreach my $role (@$disabled_roles_ar) { if ( !exists $roles->{$role} ) { Cpanel::LoadModule::load_perl_module($role); $roles->{$role} = $role->is_enabled(); } next PROFILE if $roles->{$role}; } } if ( $_META{$profile}{enabled_roles} ) { foreach my $role ( @{ $_META{$profile}{enabled_roles} } ) { if ( !exists $roles->{$role} ) { Cpanel::LoadModule::load_perl_module($role); $roles->{$role} = $role->is_enabled(); } next PROFILE if !$roles->{$role}; } } return $_CURRENT_PROFILE = $profile; } return $_CURRENT_PROFILE = Cpanel::Server::Type::Profile::Constants::STANDARD(); } sub current_profile_matches { my ($profiles_ar) = @_; $profiles_ar = [$profiles_ar] if 'ARRAY' ne ref $profiles_ar; my $current_profile = get_current_profile(); return grep { $_ eq $current_profile } @{$profiles_ar}; } my $_loaded_descriptions; sub get_meta { if ($_loaded_descriptions) { foreach my $profile ( keys %_META ) { delete @{ $_META{$profile} }{qw(name description)}; $_loaded_descriptions = 0; } } return \%_META; } sub get_meta_with_descriptions { if ( !$_loaded_descriptions ) { require 'Cpanel/Server/Type/Profile/Descriptions.pm'; ## no critic qw(Bareword) - hide from perlpkg my $add_hr = \%Cpanel::Server::Type::Profile::Descriptions::_META; foreach my $profile ( keys %$add_hr ) { @{ $_META{$profile} }{ keys %{ $add_hr->{$profile} } } = values %{ $add_hr->{$profile} }; } } return \%_META; } sub get_disabled_roles_for_profile { my ($profile) = @_; my $all_possible_roles = get_all_possible_roles(); my $meta = get_meta(); # call get_meta since it may be mocked die "No META for profile “$profile”!" if !defined $meta->{$profile}; my %profile_roles = map { $_ => 1 } ( ( $meta->{$profile}{enabled_roles} ? @{ $meta->{$profile}{enabled_roles} } : () ), ( $meta->{$profile}{optional_roles} ? @{ $meta->{$profile}{optional_roles} } : () ) ); my @disabled_roles = grep { !$profile_roles{$_} } @$all_possible_roles; return @disabled_roles ? \@disabled_roles : undef; } my $_all_possible_roles; sub get_all_possible_roles { return $_all_possible_roles if $_all_possible_roles; my $meta_std_hr = get_meta()->{ Cpanel::Server::Type::Profile::Constants::STANDARD() }; for my $nonono (qw( disabled optional )) { die "STANDARD is expected not to have “$nonono”!" if $meta_std_hr->{"${nonono}_roles"}; } return ( $_all_possible_roles = $meta_std_hr->{'enabled_roles'} ); } sub _clear_all_possible_roles { undef $_all_possible_roles; return; } sub get_service_subdomains_for_profile { my ($profile) = @_; my $meta = get_meta(); # call get_meta since it may be mocked die "No META for profile “$profile”!" if !defined $meta->{$profile}; my @profile_roles = ( ( $meta->{$profile}{enabled_roles} ? @{ $meta->{$profile}{enabled_roles} } : () ), ( $meta->{$profile}{optional_roles} ? @{ $meta->{$profile}{optional_roles} } : () ) ); require 'Cpanel/Server/Type/Change/Backend.pm'; ## no critic qw(Bareword) - hide from perlpkg my @service_subdomains; push @service_subdomains, Cpanel::Server::Type::Change::Backend::get_role_service_subs($_) for @profile_roles; return \@service_subdomains; } sub _reset_cache { undef $_CURRENT_PROFILE; return; } 1; } # --- END Cpanel/Server/Type/Profile.pm { # --- BEGIN Cpanel/Server/Type/Role/EnabledCache.pm package Cpanel::Server::Type::Role::EnabledCache; use cPstrict; use Carp (); my %_THE_CACHE; sub set ( $class, $value ) { _validate_class($class); if ( $value ne '0' && $value ne '1' ) { _confess("Value must be 0 or 1, not “$value”."); } return $_THE_CACHE{$class} = $value; } sub get ($class) { _validate_class($class); return $_THE_CACHE{$class}; } sub unset ($class) { _validate_class($class); return delete $_THE_CACHE{$class}; } sub _confess ($msg) { local $Carp::Internal{ (__PACKAGE__) } = 1; return Carp::confess($msg); } sub _validate_class ($class) { _confess("Give a class name, not $class!") if ref $class; return; } sub _unset_all () { %_THE_CACHE = (); return; } 1; } # --- END Cpanel/Server/Type/Role/EnabledCache.pm { # --- BEGIN Cpanel/Server/Type/Role.pm package Cpanel::Server::Type::Role; use strict; use warnings; # use Cpanel::Server::Type::Profile (); # use Cpanel::Server::Type::Profile::Constants (); # use Cpanel::Server::Type (); # use Cpanel::Server::Type::Role::EnabledCache (); sub new { return bless {}, $_[0]; } sub is_enabled { my ($obj_or_class) = @_; my $ref = ref($obj_or_class) || $obj_or_class; my $product_type = Cpanel::Server::Type::get_producttype(); if ( $product_type eq Cpanel::Server::Type::Profile::Constants::DNSONLY() ) { return Cpanel::Server::Type::Role::EnabledCache::set( $ref, 1 ); } if ( $product_type ne Cpanel::Server::Type::Profile::Constants::STANDARD() ) { my $META = Cpanel::Server::Type::Profile::get_meta(); return Cpanel::Server::Type::Role::EnabledCache::set( $ref, 1 ) if grep { $_ eq $ref } @{ $META->{$product_type}{enabled_roles} }; return Cpanel::Server::Type::Role::EnabledCache::set( $ref, 0 ) if !grep { $_ eq $ref } @{ $META->{$product_type}{optional_roles} }; } my $val = Cpanel::Server::Type::Role::EnabledCache::get($ref); $val //= Cpanel::Server::Type::Role::EnabledCache::set( $ref, $obj_or_class->is_available() && $obj_or_class->_is_enabled() ? 1 : 0, ); return $val; } our %_AVAILABLE_CACHE; sub is_available { my ($obj_or_class) = @_; my $ref = ref($obj_or_class) || $obj_or_class; return $_AVAILABLE_CACHE{$ref} //= $obj_or_class->_is_available(); } sub verify_enabled { my ($class) = @_; if ( !$class->is_enabled() ) { my $role = substr( $class, 1 + rindex( $class, ':' ) ); require Cpanel::Exception; die Cpanel::Exception::create( 'System::RequiredRoleDisabled', [ role => $role ] ); } return; } sub SERVICES { return [] } sub RESTART_SERVICES { return [] } sub SERVICE_SUBDOMAINS { return shift()->_SERVICE_SUBDOMAINS(); } use constant _SERVICE_SUBDOMAINS => []; sub RPM_TARGETS { return shift()->_RPM_TARGETS(); } use constant _RPM_TARGETS => []; sub _is_available { return 1 } sub _NAME { require Cpanel::Exception; die Cpanel::Exception::create( 'AbstractClass', [__PACKAGE__] ); } *_DESCRIPTION = *_NAME; 1; } # --- END Cpanel/Server/Type/Role.pm { # --- BEGIN Cpanel/Server/Type/Role/TouchFileRole.pm package Cpanel::Server::Type::Role::TouchFileRole; use strict; use warnings; # use Cpanel::Server::Type::Role(); our @ISA; BEGIN { push @ISA, qw(Cpanel::Server::Type::Role); } our $ROLES_TOUCHFILE_BASE_PATH = "/var/cpanel/disabled_roles"; sub _is_enabled { return !$_[0]->check_touchfile(); } sub check_touchfile { require Cpanel::Autodie; return Cpanel::Autodie::exists( $_[0]->_TOUCHFILE() ); } sub _TOUCHFILE { require Cpanel::Exception; die Cpanel::Exception::create( 'AbstractClass', [__PACKAGE__] ); } 1; } # --- END Cpanel/Server/Type/Role/TouchFileRole.pm { # --- BEGIN Cpanel/Server/Type/Role/MailRelay.pm package Cpanel::Server::Type::Role::MailRelay; use strict; use warnings; # use Cpanel::Server::Type::Role::TouchFileRole(); our @ISA; BEGIN { push @ISA, qw(Cpanel::Server::Type::Role::TouchFileRole); } my ( $NAME, $DESCRIPTION ); our $TOUCHFILE = $Cpanel::Server::Type::Role::TouchFileRole::ROLES_TOUCHFILE_BASE_PATH . "/mailrelay"; our $SERVICES = [ 'exim', 'exim-altport', ]; sub _NAME { require 'Cpanel/LocaleString.pm'; ## no critic qw(Bareword) - hide from perlpkg $NAME ||= Cpanel::LocaleString->new("Relay Mail"); return $NAME; } sub _DESCRIPTION { require 'Cpanel/LocaleString.pm'; ## no critic qw(Bareword) - hide from perlpkg $DESCRIPTION ||= Cpanel::LocaleString->new("This role allows users to relay email through this server."); return $DESCRIPTION; } sub _TOUCHFILE { return $TOUCHFILE; } sub SERVICES { return $SERVICES; } 1; } # --- END Cpanel/Server/Type/Role/MailRelay.pm package main;