Mini Shell
#!/usr/local/cpanel/3rdparty/bin/perl
# cpanel - ea-nginx Copyright 2020 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;
package scripts::ea_nginx;
use Cpanel::Apache::TLS ();
use Cpanel::Config::LoadCpConf ();
use Cpanel::Config::LoadUserDomains ();
use Cpanel::Config::LoadWwwAcctConf ();
use Cpanel::Config::userdata ();
use Cpanel::Config::userdata::PassengerApps ();
use Cpanel::DIp::IsDedicated ();
use Cpanel::DomainIp ();
use Cpanel::DomainLookup::DocRoot ();
use Cpanel::EA4::Conf ();
use Cpanel::EA4::Conf::Tiny ();
use Cpanel::FileGuard ();
use Cpanel::FileUtils::Move ();
use Cpanel::FileUtils::TouchFile ();
use Cpanel::HTTP::Client ();
use Cpanel::HttpUtils::ApRestart::BgSafe ();
use Cpanel::HttpUtils::Vhosts::PrimaryReader ();
use Cpanel::Hostname ();
use Cpanel::JSON ();
use Cpanel::Kernel ();
use Cpanel::NAT ();
use Cpanel::OpenSSL ();
use Cpanel::PHP::Config ();
use Cpanel::PHPFPM::Get ();
use Cpanel::PwCache ();
use Cpanel::Rand::Get ();
use Cpanel::SafeRun::Object ();
use Cpanel::Transaction::File::JSON ();
use Cpanel::Validate::URL ();
use Cpanel::Validate::Username ();
use Cpanel::WildcardDomain ();
use File::Glob ();
use File::Path::Tiny ();
use IO::Callback ();
use String::UnicodeUTF8 ();
use Template ();
use Whostmgr::TweakSettings ();
use Path::Tiny;
use MCE::Loop;
our $var_cache_ea_nginx = '/var/cache/ea-nginx';
our $var_cpanel_userdata = '/var/cpanel/userdata';
our $etc_nginx = '/etc/nginx';
our $etc_ea_nginx = "$etc_nginx/ea-nginx";
our $settings_file = "$etc_ea_nginx/settings.json";
our $cache_file = "$etc_ea_nginx/cache.json";
our $proxy_header_file = "$etc_ea_nginx/cpanel_localhost_header.json";
our $serial_touch_file = "$etc_ea_nginx/serial_config_mode";
our $skip_clearing_cache_touch_file = "$etc_ea_nginx/do_not_clear_cache_on_reload_i_understand_the_risks"; #This is highly discouraged, we do not want to document this functionality. See ZC-11741
our $wp_toolkit_bin = '/usr/local/bin/wp-toolkit';
our $wordpress_cache_dir = '/etc/nginx/wordpress_info_cache';
our $disable_wordpress_info_cache_file = '/var/cpanel/nginx_disable_wordpress_info_cache';
our $cpanel_proxy_file = '/etc/nginx/conf.d/includes-optional/cpanel-proxy-xt.conf';
our $custom_settings_dir = '/var/nginx/ea-nginx';
our $piped_module_conf = '/etc/nginx/conf.d/modules/ngx_http_pipelog_module.conf';
our $user_id_file = '/etc/cpanel/ea4/option-flags/set-USER_ID';
our $default_server_names_hash_bucket_size = 128;
# Used to validate certificates
my $openssl_obj;
my $nginxroot = "/etc/nginx";
my $cmds = {
config => {
code => \&config,
clue => "config [--all|--global|<user>] [--no-reload] [--serial]",
abstract => 'Build nginx configuration for users',
help => "Build nginx configuration for one or all users.\n\t--global will only rebuild global configurations and executes anything in /etc/nginx/ea-nginx/config-scripts/global/\n\t--all implies --global and re-does configuration for all users\n\t--serial used with --all will build the config synchronously. Otherwise, the script default is to build the config in parallel",
},
reload => {
code => \&reload,
clue => "reload",
abstract => 'reload nginx configuration',
help => "reload nginx configuration - a convenience for `restartsrv_nginx reload` (useful after multiple changes w/ --no-reload)",
},
remove => {
code => \&remove,
clue => "remove <user> [--no-reload]",
abstract => "remove a given user’s nginx configuration",
help => "Remove the given user’s nginx configuration. Only does the generated conf.d/user/<user>.conf, does not touch the user customization directory conf.d/users/<user>/",
},
clear_cache => {
code => \&clear_cache_cmd,
clue => "clear_cache [--all|<user1> <user2> ...]",
abstract => 'Clear the cache for a user, some users, or all users.',
help => 'Clears a cache or caches. Pass the users you want to clear or none for all users.',
},
cache => {
code => \&cache_config,
clue => "cache [--system|<user>] [--no-rebuild] [[--reset|--enabled=[1|0]]]",
abstract => "Manage cache configuration at the system or user level",
help => "View, configure, reset NGINX caching at the system or user level\n\n\t--no-rebuild will make it skip the config regeneration and NGINX reload necessary to make the changes take effect\n\n\t--reset w/ --system will reset the system level config to the defaults\n\t--reset w/ a user will remove the user’s config effectively resetting it to the system level config\n",
},
};
my $hint_blurb = "Usage: `$0 {command} …`.\n\tThis tool supports the following commands:";
my $opts = {
'help:pre_hint' => $hint_blurb,
'help:pre_help' => "Various ea-nginx related admin utilities\n\n$hint_blurb",
default_commands => "help",
alias => { conf => "config" },
'help:post_help' => "More information on `ea-nginx` (what works and what does not, how to customize configuration, etc)\n can be found at https://go.cpanel.net/ea-nginx.",
};
# Cache for proxied domains
my %apache2nginx_proxied_domains;
sub _load_apache2nginx_proxied_domains {
my $server_json_path = '/var/lib/apache2nginx/server.json';
if (-e $server_json_path) {
my $data;
$data = Cpanel::JSON::LoadFile($server_json_path);
foreach my $domain (keys %{$data}) {
# Check if 'proxy_reason' exists, is defined, and not empty
if (exists $data->{$domain}{'proxy_reason'} && defined $data->{$domain}{'proxy_reason'} && $data->{$domain}{'proxy_reason'} ne '') {
$apache2nginx_proxied_domains{$domain} = 1;
print "Domain '$domain' marked as proxied due to proxy_reason.\n";
}
else {
print "Domain '$domain' does not have a valid proxy_reason; not marked as proxied.\n";
}
}
}
else {
warn "server.json not found at $server_json_path. No domains will be excluded from redirects based on proxy_reason.\n";
}
}
# Initialize apache2nginx_proxied_domains at script start
_load_apache2nginx_proxied_domains();
run(@ARGV) if !caller;
=encoding utf-8
=head1 NAME
scripts::ea_nginx
=head1 SYNOPSIS
ea-nginx [ cache|clear_cache|config|reload|remove|help ]
NOTE: Execute the script with --help for more details or go to https://go.cpanel.net/ea-nginx
Can also execute 'ea-nginx [help|hint] subcommand' for targeted details about a subcommand
=head1 DESCRIPTION
This script provides various ea-nginx related admin utilities
=over
=item cache subcommand
View, configure, reset NGINX caching at the system or user level
=item clear_cache subcommand
Clears a cache or caches. Pass the users you want to clear or none for all users
=item config subcommand
Build nginx configuration for one or all users
=item reload subcommand
Reload nginx configuration
=item remove subcommand
Remove the given user’s nginx configuration
=back
=cut
sub run {
my (@argv) = @_;
die "This script should only be called as root\n" if $> != 0;
local $ENV{TERM} = $ENV{TERM} || "xterm-256color"; # non-CLI modulino avoid needless: Cannot find termcap: TERM not set at …/Term/ReadLine.pm line 373.
require App::CmdDispatch;
import App::CmdDispatch;
# need to have App::CmdDispatch do this automatically see CPANEL-22328
if ( @argv && grep { defined && m/\A\-\-help\z/ } @argv ) {
App::CmdDispatch->new( $cmds, $opts )->help();
exit(0);
}
my $orig_command_hint = \&App::CmdDispatch::command_hint;
no warnings "redefine";
local *App::CmdDispatch::command_hint = sub {
$orig_command_hint->(@_);
exit(1);
};
no warnings 'once';
require App::CmdDispatch::IO;
local *App::CmdDispatch::IO::print = sub {
shift;
if ( ref($@) && $@ =~ m/^App::CmdDispatch::Exception/ ) {
CORE::print STDERR @_;
return;
}
CORE::print(@_);
return;
};
use warnings 'once';
# ^^^ /need to have App::CmdDispatch do this automatically see CPANEL-22328
# Enforce acquiring a lock before continuing so that two competing processes
# do not stomp on each other
my $lock_dir = '/var/cpanel/locks';
mkdir( $lock_dir, 0700 ) unless -e $lock_dir;
my $nginx_config_lock = Cpanel::FileGuard->new("$lock_dir/ea-nginx-config-in-progress");
# allows customers to set custom configs before ea-nginx is installed
_update_for_custom_configs();
return App::CmdDispatch->new( $cmds, $opts )->run(@argv);
}
################
#### commands ##
################
=head2 remove()
Remove the given user’s nginx configuration
=over
=item user - required
A valid cPanel username
=item --no-reload - optional
Whether to reload nginx and clear the cache after removing the user
=back
=cut
sub remove {
my ( $app, $user, @args ) = @_;
_validate_user_arg( $app, $user );
my $file = "$nginxroot/conf.d/users/$user.conf";
my $dir = "$nginxroot/conf.d/users/$user/";
if ( -f $file ) {
print "Removing $file …\n";
unlink $file;
if ( -f $file ) {
die "Unable to remove $file\n";
}
unless ( grep { $_ eq '--no-reload' } @args ) {
_reload();
clear_cache($user);
}
}
else {
print "$file is already removed or never existed.\n";
}
if ( -d $dir ) {
warn "Customization path $dir exists, you will need to manually move/remove/reconfigure that.\n";
}
else {
print "Customization path $dir does not exist. You are all set!\n";
}
return;
}
=head2 config()
Build nginx configuration for one or all users.
=over
=item --all
--all implies --global and re-does configuration for all users
=item --global
--global will only rebuild global configurations and executes anything in
/etc/nginx/ea-nginx/config-scripts/global/
=item user
A valid cPanel username
=back
=over
=item --no-reload - optional
Whether to reload nginx and clear the cache after making config changes
=item --serial - optional
--serial used with --all will build the config synchronously. Otherwise, the
script default is to build the config in parallel
=back
=cut
sub config {
my ( $app, $user, @args ) = @_;
my $reload = grep( { $_ eq '--no-reload' } @args ) ? 0 : 1;
my $use_serial_mode = grep( { $_ eq '--serial' } @args ) ? 1 : 0;
$use_serial_mode = 1 if -e $serial_touch_file;
if ( $user eq '--all' || $user eq '--global' ) {
_write_global_cpanel_localhost();
_write_global_logging();
_write_global_passenger();
_write_global_ea_nginx();
_write_global_nginx_conf();
_write_global_default();
_write_global_cpanel_proxy_non_ssl();
_do_other_global_config();
my $errors = {};
if ( $user eq '--all' ) {
_populate_wordpress_cache( { all => 1 } );
my $global_config_data = _get_global_config_data();
mkdir "/etc/nginx/conf.d/users/";
unlink( File::Glob::bsd_glob("/etc/nginx/conf.d/users/*.conf") );
$errors =
$use_serial_mode
? _update_user_configs_in_serial_mode($global_config_data)
: _update_user_configs_in_parallel_mode($global_config_data);
}
if ($reload) {
_reload();
clear_cache() if !-e $skip_clearing_cache_touch_file;
}
if ( keys %$errors ) {
my $err = "";
for my $usr ( sort keys %$errors ) {
$err .= "\tUser “$usr”: $errors->{$usr}\n\n";
}
die "The following user related errors need attention:\n$err";
}
}
else {
_validate_user_arg( $app, $user );
mkdir "/etc/nginx/conf.d/users/";
# Settings in this file can be affected when a new user is added
_write_global_ea_nginx();
_populate_wordpress_cache( { user => $user } );
my $global_config_data = _get_global_config_data();
_write_user_conf( $user, $global_config_data );
if ($reload) {
_reload("/etc/nginx/conf.d/users/$user.conf");
clear_cache($user) if !-e $skip_clearing_cache_touch_file;
}
}
return 1;
}
=head2 cache_config()
View, configure, or reset NGINX caching at the system or user level
=over
=item --system
View, configure, or reset the global defaults
=item user
View, configure, or reset the NGINX caching config for the given user
=back
=over
=item --no-rebuild - optional
Will make it skip the config regeneration and NGINX reload necessary to make the
changes take effect
=item --reset - optional
=over 2
=item with --system
Will reset the system level config to the defaults
=item with a user
Will remove the user’s config effectively resetting it to the system level
config
=back
=item --enabled=[1|0] - optional
=over 2
=item with --system
Will enable/disable caching for users using the global default
=item with user
Will enable/disable caching for the user
=back
=back
=cut
sub cache_config {
my ( $app, $which, @flags ) = @_;
die "First argument to `cache` must be `--system` or a cPanel user name\n" if !length $which;
my $file = $cache_file;
my $config_arg = "--all";
if ( $which ne "--system" ) {
_validate_user_arg( $app, $which );
$file = $var_cpanel_userdata . "/$which/nginx-cache.json";
$config_arg = $which;
}
if ( !@flags ) {
print eval { path($file)->slurp } || "{}\n";
}
else {
require Getopt::Param::Tiny;
my $prm = Getopt::Param::Tiny->new(
{
array_ref => \@flags,
known_only => [qw(reset enabled no-rebuild)],
help_coderef => sub { $app->help(); exit(1) },
validate => sub {
my ($prm) = @_;
my @given = grep { $_ ne "reset" && $_ ne "no-rebuild" } @{ $prm->param() };
die "--reset does not make sense w/ other flags (besides --no-rebuild)\n" if $prm->param('reset') && @given;
return 1;
},
}
);
if ( $prm->param('reset') ) {
if ( $which eq "--system" ) {
my %caching_defaults = caching_defaults();
_jsonify_caching_booleans( \%caching_defaults );
_write_json( $file, \%caching_defaults );
}
else {
unlink $file;
}
}
else {
my $conf_hr = eval { Cpanel::JSON::LoadFile($file) } || {};
# always set it since its the only option until ZC-8549
$conf_hr->{enabled} = _bool_param( enabled => scalar( $prm->param('enabled') ) );
# ZC-8549 example:
# my %given_params;
# @given_params{ $prm->param() } = ();
# for my $bool (qw(enabled x_cache_header logging)) {
# next if !exists $given_params{$bool}; # only set it if given
# $conf_hr->{$bool} = _bool_param( $bool => scalar($prm->param($bool)) );
# }
#
# if (exists $given_params{zone_size}) {
# my $zone_size = $prm->param('zone_size');
# die "Invalid --zone_size, it must …\n" if … $zone_size;
# $conf_hr->{zone_size} = $zone_size;
# }
_jsonify_caching_booleans($conf_hr);
_write_json( $file, $conf_hr );
}
config( $app, $config_arg ) unless $prm->param("no-rebuild");
}
return 1;
}
=head2 clear_cache_cmd()
Clears a cache or caches. Pass the users you want to clear or none for all
users.
=over
=item --all - optional
Clear the cache for all users
=item user1 user2 user3 …
A list of users to clear the cache for
=back
=cut
sub clear_cache_cmd {
my ( $app, @users ) = @_;
if ( @users == 0 || $users[0] eq '--all' ) {
shift @users;
die "--all can not be mixed with usernames\n" if @users;
}
else {
foreach my $user (@users) {
_validate_user_arg( $app, $user );
}
}
clear_cache(@users);
return 1;
}
=head2 clear_cache()
Clear the cache for a given list of users if received. Otherwise, clear the
cache for all users.
=over
=item users - optional
A list of cPanel users to clear the cache for
=back
=cut
sub clear_cache {
my (@users) = @_;
if (@users) {
foreach my $user (@users) {
_delete_glob( $var_cache_ea_nginx . "/*/$user/*" );
}
}
else {
_delete_glob( $var_cache_ea_nginx . "/*/*/*" );
}
return 1;
}
=head2 reload()
Reload nginx configuration
=cut
sub reload {
_reload();
clear_cache() if !-e $skip_clearing_cache_touch_file;
}
###############
#### helpers ##
###############
=head2 _wants_http2()
Returns whether http2 is enabled or not.
=head3 RETURNS
Returns 1 if http2 is enabled. 0 otherwise.
=cut
our $wants_http2;
sub _wants_http2 {
if ( !defined $wants_http2 ) {
$wants_http2 = -e "/etc/nginx/conf.d/http2.conf" ? 1 : 0;
}
return $wants_http2;
}
=head2 _has_ipv6()
Returns whether IPv6 is enabled or not.
=head3 RETURNS
Returns 1 if IPv6 is enabled. 0 otherwise.
=cut
our $has_ipv6;
sub _has_ipv6 {
$has_ipv6 //= -f '/proc/net/if_inet6' ? 1 : 0;
return $has_ipv6;
}
=head2 ensure_valid_nginx_config()
Attempt to verify that the nginx binary reports that it has a valid
configuration in place. Attempt to fix any errors that the nginx binary reports.
Dies if it is unable to verify a valid nginx configuration. This effectively
prevents the script from reloading nginx with a bad configuration and gives a
warning to the user that they will need to manually resolve the issue before
reloading nginx.
=cut
sub ensure_valid_nginx_config {
my ($depth) = @_;
$depth //= 0;
# limit recursion depth to 25
my $max_depth = 25;
my $nginx_bin = _get_nginx_bin();
my $combined_output = '';
my $output_handler = IO::Callback->new( '>', sub { $combined_output .= shift } );
my $run = Cpanel::SafeRun::Object->new(
program => $nginx_bin,
args => ['-t'],
stdout => $output_handler,
stderr => $output_handler,
);
my $resolved_issue;
if ( $run->CHILD_ERROR() ) {
$resolved_issue = _attempt_to_fix_syntax_errors($combined_output);
}
else {
print "Verified valid syntax for nginx configuration\n";
return;
}
( $resolved_issue && $depth < $max_depth )
? return ensure_valid_nginx_config( ++$depth )
: die "Unable to ensure a valid nginx configuration:\n\n$combined_output\n\nYou will need to manually resolve this issue.\n";
return;
}
=head2 caching_defaults()
Returns a hash with the hardcoded caching fallbacks in case the user / global
defined files are either missing altogether or missing a setting. The return
changes depending on whether micro caching is enabled or not. Micro caching
can be enabled by putting the following touch file in place:
/etc/nginx/ea-nginx/enable.micro-cache-defaults
=head3 RETURNS
A hash of hardcoded caching fallbacks.
=cut
sub caching_defaults { # do not want `our %caching_defaults` or a hash reference so that no one can accidentally change it
# non-micro
my %defaults = (
proxy_cache_valid => {
"200 301 302" => "60m",
"404" => "1m",
},
);
if ( -e '/etc/nginx/ea-nginx/enable.micro-cache-defaults' ) {
# micro
%defaults = (
proxy_cache_valid => {
"301 302" => "5m",
"404" => "1m",
},
);
}
# hard coded fallback in case they remove stuff from /etc/nginx/ea-nginx/cache.json
return (
%defaults,
enabled => 1,
logging => 0,
x_cache_header => 0,
zone_size => "10m",
inactive_time => "60m",
levels => "1:2",
proxy_cache_use_stale => "error timeout http_429 http_500 http_502 http_503 http_504",
proxy_cache_background_update => "on",
proxy_cache_revalidate => "on",
proxy_cache_min_uses => 1,
proxy_cache_lock => "off",
);
}
sub _bool_param {
my ( $flag, $value ) = @_;
die "--$flag value must be 1 or 0\n" if !defined $value || ( $value ne "1" && $value ne "0" );
return $value;
}
sub _jsonify_caching_booleans {
my ($hr) = @_;
require JSON::PP;
for my $boolkey (qw(enabled logging x_cache_header)) {
next if !exists $hr->{$boolkey};
$hr->{$boolkey} = $hr->{$boolkey} ? JSON::PP::true() : JSON::PP::false();
}
return;
}
=head2 _write_json()
Given a filename and a hashref of data to write to it, it will write the data
to the file in json pretty format.
=over
=item file
A valid filename
=item ref
A hashref containing data to write to the filename
=back
=cut
sub _write_json {
my ( $file, $ref ) = @_;
my $transaction = Cpanel::Transaction::File::JSON->new( path => $file, "permissions" => 0644 );
$transaction->set_data($ref);
$transaction->save_pretty_canonical_or_die();
$transaction->close_or_die();
return 1;
}
=head2 _render_tt_to_file()
Consume a given template toolkit file and produce a given output file with the
given data.
=over
=item tt_file
The path to the template toolkit file to consume located within
'/etc/nginx/ea-nginx/'
=item output_file
The path of the configuration file to output to within the '/etc/nginx/conf.d/'
directory.
=item tt_data
A hashref of data that will be used by the template toolkit file.
=back
=cut
my $tt;
sub _render_tt_to_file {
my ( $tt_file, $output_file, $tt_data ) = @_;
$tt ||= Template->new( { INCLUDE_PATH => "/etc/nginx/" } );
my $output_path = "/etc/nginx/conf.d/$output_file";
path($output_path)->touchpath;
my $output_tt = path("/etc/nginx/ea-nginx/$tt_file")->slurp;
$tt->process(
\$output_tt, $tt_data,
sub {
my ($out) = @_;
path($output_path)->spew($out);
return 1;
}
);
if ( $tt->error() ) {
my $tt_err = $tt->error();
unlink $output_path;
die "$tt_err\nFailed to ensure “$output_path” does not exist: $!\n" if -e $output_path;
die $tt_err;
}
return 1;
}
=head2 _do_other_global_config()
Allow for integrators to customize the configuration. It consumes and executes
any executable scripts within the '/etc/nginx/ea-nginx/config-scripts/global'
directory. It also gets called after all other global configurations meaning
that it can overwrite them if the integrator wishes for the scripts to do so.
=cut
sub _do_other_global_config {
# /etc/nginx/ea-nginx/config-scripts/global/* allows for future
# _do_other_config_user($user) via /etc/nginx/ea-nginx/config-scripts/user/*
for my $script ( File::Glob::bsd_glob("/etc/nginx/ea-nginx/config-scripts/global/*") ) {
# Seen during the install of ea-modsec30-connector-nginx
# During the install of multiple packages, the script
# - May not actually be completely there it ends in .dpkg-new
# - May not be executable
if ( -x $script && $script !~ m/dpkg-new$/ ) {
print "Running (global) “$script” …\n";
system($script) && warn "“$script” did not exit clean\n";
}
else {
print "Skipping “$script” as it is not executable\n";
}
}
return;
}
=head2 _get_application_paths()
Populates a given hashref with the path to the binaries for the global default
versions of ruby, python, and nodejs.
=over
=item app
A hashref to populate with the paths to the binaries. Note, the given hashref
will be altered by this sub.
=back
=cut
sub _get_application_paths {
my ($app) = @_;
return Cpanel::Config::userdata::PassengerApps->ensure_paths( $app, 0 );
}
=head2 _write_global_cpanel_localhost()
Write '/etc/nginx/conf.d/includes-optional/cpanel-proxy-xt.conf' with a unique
cPanel-localhost header. The cPanel-localhost will be refreshed if the file
that it is stored in does not exist or has an mtime more than 30 minutes old.
=cut
sub _write_global_cpanel_localhost {
# regen new value if its not set or if its been at least half an hour
_write_cpanel_localhost_header() if !-e $proxy_header_file || ( stat(_) )[9] < ( time() - 1800 );
my $conf = Cpanel::JSON::LoadFile($proxy_header_file);
my $val = $conf->{'cPanel-localhost'} || _write_cpanel_localhost_header();
my $path = Path::Tiny::path($cpanel_proxy_file);
my $contents = <<"CPANEL_PROXY";
proxy_set_header cPanel-localhost $val;
# for secure use of proxying to Apache w/ mod_remoteip
proxy_headers_hash_bucket_size 128;
proxy_set_header X-Forwarded-For-$val \$proxy_add_x_forwarded_for;
CPANEL_PROXY
$path->spew($contents);
$path->chmod(0600);
return;
}
=head2 _write_cpanel_localhost_header()
Generate a new cPanel-localhost header, write the cPanel-localhost header to
/etc/nginx/ea-nginx/cpanel_localhost_header.json, and rebuild and restart
apache so that it can pick up the new cPanel localhost header that nginx will
send.
=head3 RETURNS
Returns the new cPanel localhost header value.
=cut
sub _write_cpanel_localhost_header {
my $rand = Cpanel::Rand::Get::getranddata(64);
# Though technically legal, NGINX does not like _ in header names (for good reason honestly)
# and we want to do things like: proxy_set_header X-Forwarded-For-<RAND> …;
$rand =~ s/_/-/g;
my %headers = (
'cPanel-localhost' => $rand,
);
_write_json( $proxy_header_file, \%headers );
chmod 0600, $proxy_header_file; # in case it is reset out form under the package
_rebuild_and_restart_apache(); # needed so that apache can pick up the new header file
return $headers{"cPanel-localhost"};
}
=head2 _write_global_nginx_conf()
Update '/etc/nginx/nginx.conf' with the custom values for 'worker_processes'
and 'worker_shutdown_timeout' if any. Otherwise, update it with a default
value for these two configuration variables.
=cut
sub _write_global_nginx_conf {
my $main = path("/etc/nginx/nginx.conf");
my $cont = $main->slurp;
my $local_settings = _get_settings_hr();
# worker_processes
my $val = "1";
if ( exists $local_settings->{worker_processes} ) {
if ( $local_settings->{worker_processes} =~ m/^[1-9][0-9]*$/ || $local_settings->{worker_processes} eq "auto" ) {
$val = $local_settings->{worker_processes};
}
else {
warn "Custom `worker_processes` in “$settings_file” is not a number (and is not “auto”), falling back to “$val” …\n";
}
}
$cont =~ s/^(\s*worker_processes\s+)\S+(\s*;)/$1$val$2/gm;
# worker_shutdown_timeout
$val = "10s";
if ( exists $local_settings->{worker_shutdown_timeout} ) {
if ( $local_settings->{worker_shutdown_timeout} =~ m/^[1-9][0-9]*(?:ms|[smhdwMy])?$/ ) {
$val = $local_settings->{worker_shutdown_timeout};
}
else {
warn "Custom `worker_shutdown_timeout` in “$settings_file” is not an NGINX time value, falling back to “$val” …\n";
}
}
$cont =~ s/^(\s*worker_shutdown_timeout\s+)\S+(\s*;)/$1$val$2/gm;
$main->spew($cont);
return 1;
}
=head2 _write_global_cpanel_proxy_non_ssl()
Update '/etc/nginx/conf.d/cpanel-proxy-non-ssl.conf' with the correct values for
IPv6 and USER_ID.
=cut
sub _write_global_cpanel_proxy_non_ssl {
my $ipv6 = _has_ipv6();
my $prefix = $ipv6 ? "" : "# server does not have IPv6 enabled: ";
my $cpns = path("/etc/nginx/conf.d/cpanel-proxy-non-ssl.conf");
my $content = $cpns->slurp;
# toggle IPv6
$content =~ s/^.*(listen\s+\[::\]:)/ $prefix$1/gm;
my $user_id_prefix = _wants_user_id() ? '' : '# ';
# toggle $USER_ID
$content =~ s/^.*(set\s+\$USER_ID\s+""\s*;)/ $user_id_prefix$1/gm;
$cpns->spew($content);
return 1;
}
=head2 _write_global_default()
Update '/etc/nginx/conf.d/default.conf' with the correct values for IPv6, http2,
USER_ID, reuseport, and the correct default ssl certificate to use on the system.
=cut
sub _write_global_default {
my $reuseport = Cpanel::Kernel::system_is_at_least('3.9.0') ? 1 : 0;
my $ipv6 = _has_ipv6();
my $http2 = _wants_http2();
my $wants_uid = _wants_user_id();
my $ssl_certificate =
-f '/var/cpanel/ssl/cpanel/mycpanel.pem'
? '/var/cpanel/ssl/cpanel/mycpanel.pem'
: '/var/cpanel/ssl/cpanel/cpanel.pem';
return _render_tt_to_file(
'default.conf.tt' => 'default.conf',
{
reuseport => $reuseport,
ipv6 => $ipv6,
http2 => $http2,
ssl_certificate => $ssl_certificate,
ssl_certificate_key => $ssl_certificate,
uid => $wants_uid,
},
);
}
=head2 _write_global_passenger()
Write '/etc/nginx/conf.d/passenger.conf' with the correct application paths for
ruby, python, and nodejs
=cut
sub _write_global_passenger {
unlink '/etc/nginx/conf.d/passenger.conf'; # will get re-written fresh below if its needed, otherwise we want it gone
return 1 unless -e '/etc/nginx/modules/ngx_http_passenger_module.so';
my $defaults_hr = { name => "global passenger defaults" };
_get_application_paths($defaults_hr);
my $passenger_root = $defaults_hr->{ruby};
$passenger_root =~ s{/[^/]+$}{/../share/passenger/phusion_passenger/locations.ini};
my $passenger_instance_registry_dir = $defaults_hr->{ruby};
if ( $passenger_instance_registry_dir =~ m{^/opt/cpanel} ) {
$passenger_instance_registry_dir =~ s{/[^/]+$}{/../../var/run/passenger-instreg};
}
else {
$passenger_instance_registry_dir = '/opt/cpanel/ea-passenger/run/passenger-instreg';
}
File::Path::Tiny::mk($passenger_instance_registry_dir);
return _render_tt_to_file(
'ngx_http_passenger_module.conf.tt' => 'passenger.conf',
{
passenger => {
global => {
passenger_root => $passenger_root,
passenger_instance_registry_dir => $passenger_instance_registry_dir,
default => $defaults_hr,
},
},
},
);
}
=head2 _get_settings_hr()
Returns a hashref containing nginx settings based on the default settings, the
contents of '/etc/nginx/ea-nginx/settings.json', and the calculated values of
'server_names_hash_bucket_size', and 'server_names_hash_max_size'. The result
of this sub is cached and the cached result will be returned after the first
call.
=head3 RETURNS
A hashref similar to the following:
{
apache_port_ip => '127.0.0.1',
server_names_hash_bucket_size => 128,
server_names_hash_max_size => 1024,
apache_ssl_port_ip => '127.0.0.1',
client_max_body_size => '128m',
apache_port => 81,
apache_ssl_port => 444,
}
=cut
our $settings_hr;
sub _get_settings_hr {
if ( !defined $settings_hr ) {
my $global_settings = Cpanel::JSON::LoadFile($settings_file);
$settings_hr = {
_settings_defaults(),
%{$global_settings},
};
# Do this here so that these two settings are always calculated
# NOTE: The calculations are based on the advice given here: http://nginx.org/en/docs/http/server_names.html#optimization
$settings_hr->{server_names_hash_max_size} = _get_server_names_hash_max_size();
$settings_hr->{server_names_hash_bucket_size} = _get_server_names_hash_bucket_size( $settings_hr->{server_names_hash_max_size} );
}
return $settings_hr;
}
=head2 _get_server_names_hash_bucket_size()
Calculate the value of 'server_names_hash_bucket_size' based on the length of
the longest domain hosted on the server.
=head3 RETURNS
An integer greater than or equal to 128. The integer must be a power of 2.
=cut
sub _get_server_names_hash_bucket_size {
my ($server_names_hash_max_size) = @_;
my $server_names_hash_bucket_size = $default_server_names_hash_bucket_size;
my $domain_length_info = _get_domain_length_info();
# server_names_hash_bucket_size should be power of 2 for CPU cache line size
# optimization purposes. Set it to first power of 2 larger then the longest
# server_name or 128 if it is sufficient
# As a sanity check, we also need to ensure that
# server_names_hash_max_size * server_names_hash_bucket_size
# is larger than the total bytes taken up by all the domains combined
my $server_names_total_size = $server_names_hash_max_size * $server_names_hash_bucket_size;
while ( $server_names_hash_bucket_size < $domain_length_info->{longest} || $server_names_total_size < $domain_length_info->{total_length} ) {
$server_names_hash_bucket_size *= 2;
$server_names_total_size = $server_names_hash_max_size * $server_names_hash_bucket_size;
}
return $server_names_hash_bucket_size;
}
=head2 _get_aligned_domain_length()
Given an integer, it returns the smallest integer that is larger than the
integer and divisible by 8.
=over
=item domain_length
An unsigned integer to align to 8
=back
=head3 RETURNS
The smallest integer that is larger than the given integer and divisible by 8
=cut
sub _get_aligned_domain_length {
my ($domain_length) = @_;
my $align_to = 8; # Assumes x86_64
my $remainder = $domain_length % $align_to;
# if there is a remainder, then we need to align the domain length
# otherwise, it is already divisible by 8 and there is nothing to do
my $aligned_domain_length = $remainder ? $domain_length + $align_to - $remainder : $domain_length;
return $aligned_domain_length;
}
=head2 _get_domain_length_info()
Returns a hashref containing two keys that represent that have values
representing the length of the longest domain on the server and the total length
of all domains hosted on the server
=head3 RETURNS
A hashref similar to the following:
{
longest => 200,
total_length => 4242,
}
=cut
sub _get_domain_length_info {
# If a miss is encountered while building the server_names_hash, then up to
# 5/8 of the hash length could be unused
my $nginx_buffer = $default_server_names_hash_bucket_size / 8 * 5;
my $service_subdomain_buffer = 16; # Add padding for service subdomains (autodiscover. is currently the longest)
my $longest = 0;
my $total_length = 0;
my $user_lookup = _get_user_domains();
for my $usr ( sort keys %$user_lookup ) {
for my $domain ( @{ $user_lookup->{$usr} } ) {
my $domain_length = length $domain;
my $aligned_domain_length = _get_aligned_domain_length($domain_length);
my $total_domain_length = $aligned_domain_length + $nginx_buffer + $service_subdomain_buffer;
$longest = $total_domain_length if $total_domain_length > $longest;
# There are up to 10 copies of the domain generated between the two
# server_name blocks for the domain. On average, this number is 8,
# but we go with the larger number here to be sure that we the
# configure nginx with large enough values to generate its
# server_names hash
$total_domain_length *= 10;
$total_length += $total_domain_length;
}
}
my $domain_length_info = {
longest => $longest,
total_length => $total_length,
};
return $domain_length_info;
}
=head2 _get_server_names_hash_max_size()
Calculates the value of 'server_names_hash_max_size' based on the total number
of domains hosted on the server.
=head3 RETURNS
An integer greater than or equal to 1024.
=cut
sub _get_server_names_hash_max_size {
my $minimum_hash_max_size = 1024;
my $num_domains = _get_num_total_domains();
# We generate ~8 server blocks for each domain
my $num_server_blocks = $num_domains * 8;
# At a minimum, 'server_names_hash_max_size' needs to match the number of server blocks generated
# If the number of server blocks to generate is larger than the minimum that we allow this setting to be
# then increase the setting size to the number of server blocks to generate
# otherwise, honor the minimum size
return ( $minimum_hash_max_size < $num_server_blocks ) ? $num_server_blocks : $minimum_hash_max_size;
}
=head2 _write_global_ea_nginx()
Uses template toolkit file to write '/etc/nginx/conf.d/ea-nginx.conf'
=cut
sub _write_global_ea_nginx {
my $cur_settings = _get_settings_hr();
my $ea4_conf_hr = Cpanel::EA4::Conf->instance->as_hr();
my $vhosts = _get_httpd_vhosts_hash();
my %ips = map { $vhosts->{$_}{ip} => 1 } keys %{$vhosts};
return _render_tt_to_file(
'ea-nginx.conf.tt' => 'ea-nginx.conf',
{ ea4conf => $ea4_conf_hr, settings => $cur_settings, ips => [ sort keys %ips ] }
);
}
=head2 _settings_defaults()
Returns a hash containing the default settings for nginx. This is needed as a
fallback in case '/etc/nginx/ea-nginx/settings.json' is missing a crucial
value needed for rendering the template toolkit for 'ea-nginx.conf'.
=cut
sub _settings_defaults {
my $cpconf_hr = _get_cpconf_hr();
my %ports;
foreach my $key ( 'apache_port', 'apache_ssl_port' ) {
$ports{$key} = $cpconf_hr->{$key} =~ /:([0-9]+)$/ ? $1 : 0;
if ( $ports{$key} < 1 || $ports{$key} > 65535 ) { # last ditch fall back if cpconf has an invalid value
$ports{$key} = $key eq 'apache_port' ? 81 : 444;
}
}
# hard coded fallback in case they remove stuff from /etc/nginx/ea-nginx/settings.json
return (
apache_port => $ports{apache_port},
apache_port_ip => '127.0.0.1',
apache_ssl_port => $ports{apache_ssl_port},
apache_ssl_port_ip => '127.0.0.1',
client_max_body_size => '128m',
);
}
=head2 _write_global_logging()
Uses template toolkit file to write '/etc/nginx/conf.d/global-logging.conf' and
enables/disables piped logging based on the global system setting
=cut
sub _write_global_logging {
my $logging_hr = _get_logging_hr();
_render_tt_to_file(
'global-logging.tt' => 'global-logging.conf',
{
logging => $logging_hr,
hostname => scalar( Cpanel::Hostname::gethostname() ),
},
);
if ( $logging_hr->{piped_logs} ) {
path($piped_module_conf)->spew("load_module modules/ngx_http_pipelog_module.so;");
}
else {
unlink $piped_module_conf;
die "Failed to ensure “$piped_module_conf” does not exist: $!\n" if -e $piped_module_conf;
}
return 1;
}
=head2 _reload()
Reload nginx configuration
=over
=item new_file - optional
If nginx fails to reload, then this file will be removed and second attempt to
reload nginx without this file in place will take place
=back
=cut
sub _reload {
my ($new_file) = @_;
ensure_valid_nginx_config(); # dies if it fails to ensure a valid config
if ( system("/usr/local/cpanel/scripts/restartsrv_nginx reload") != 0 ) {
if ($new_file) {
warn "Could not reload generated nginx config, removing and attempting reload without it: $?\n";
unlink $new_file;
system("/usr/local/cpanel/scripts/restartsrv_nginx reload") || return 1;
}
exit 1;
}
return 1;
}
=head2 _validate_domains_data_or_die( $user, $domains_data )
Validates that the given user was able to load its domain userdata and that the
domain userdata has the minimum expected data
=over
=item user
The username that is being validated
=item domains_data
A hashref containing the domain data that is being validated
=back
=cut
sub _validate_domains_data_or_die {
my ( $user, $domains_data ) = @_;
die "Failed to load user ‘$user’s web vhost data\n" unless $domains_data;
die "No vhosts config data for user ‘$user’\n" unless $domains_data->{main_domain};
return;
}
=head2 _validate_user_uid_and_gid($user)
Validates that the given user has a valid system UID and GID
=over
=item user
The username being validated
=back
=cut
sub _validate_user_uid_and_gid {
my ($user) = @_;
die "Unable to determine user id for $user, assuming invalid user and skipping\n" unless ( Cpanel::PwCache::getpwnam($user) )[2];
die "Unable to determine group id for $user, assuming invalid user and skipping\n" unless _get_group_for($user);
return;
}
=head2 _write_user_conf()
Writes the configuration for the given user. This includes the caching
configuration, server block for the primary and parked domains, and the server
blocks for any subdomains and/or addon domains belonging to the user.
=over
=item user
A valid cPanel username
=item global_config_data
A hashref containing data that should be treated as read only that all users
can use to build their configs with.
=back
=cut
sub _write_user_conf {
my ( $user, $global_config_data ) = @_;
_validate_user_uid_and_gid($user);
my $domains_data = Cpanel::Config::userdata::Load::load_userdata_main($user);
_validate_domains_data_or_die( $user, $domains_data );
my $userdata_for_main_domain = _get_userdata_for( $user => $domains_data->{main_domain} );
my %addon_subdomains = map { $domains_data->{addon_domains}{$_} => 1 }
grep { $domains_data->{addon_domains}{$_} }
keys %{ $domains_data->{addon_domains} };
my @actual_subdomains = grep { $_ && !exists $domains_data->{addon_domains}{$_} && !$addon_subdomains{$_} } @{ $domains_data->{sub_domains} };
my %subdomains_hash = map { $_ => 1 } @{ $domains_data->{sub_domains} };
my $userconf = "/etc/nginx/conf.d/users/$user.conf";
print "Setting up $userconf …\n";
if ( !defined &scripts::ea_nginx_userdata::run ) {
my $prefix = "/usr/local/cpanel/scripts/ea-nginx";
if ( $0 =~ m{/SOURCES/} ) {
# Can’t use FindBin because the script name is different in repo and in RPM
require Cwd;
$prefix = Cwd::abs_path($0);
}
require "$prefix-userdata";
}
scripts::ea_nginx_userdata::run($user);
my $caching_hr = _get_caching_hr($user);
my $pre_server = "";
if ( $caching_hr->{enabled} ) {
$pre_server = "proxy_cache_path /var/cache/ea-nginx/proxy/$user levels=$caching_hr->{levels} keys_zone=$user:$caching_hr->{zone_size} inactive=$caching_hr->{inactive_time};\n\n";
}
my $domains = [
$domains_data->{main_domain},
'www.' . $domains_data->{main_domain},
];
push( @$domains, "mail.$domains_data->{main_domain}" ) unless $subdomains_hash{"mail.$domains_data->{main_domain}"};
push( @$domains, "ipv6.$domains_data->{main_domain}" ) if exists $userdata_for_main_domain->{ipv6};
foreach my $dom ( @{ $domains_data->{parked_domains} } ) {
push @$domains, $dom;
if ( !Cpanel::WildcardDomain::is_wildcard_domain($dom) ) {
push @$domains, "www.$dom";
push( @$domains, "mail.$dom" ) unless $subdomains_hash{"mail.$dom"};
}
}
path($userconf)->spew( $pre_server . "#### main domain for $user ##\n" );
_render_and_append(
{
user => $user,
domains => $domains,
global_config_data => $global_config_data,
}
);
_init_logs_for( $user, $domains_data->{main_domain} );
if (@actual_subdomains) {
path($userconf)->append("\n#### sub domains for $user ##\n");
for my $subdom (@actual_subdomains) {
my $subdoms = [$subdom];
push( @$subdoms, "www.$subdom" ) unless Cpanel::WildcardDomain::is_wildcard_domain($subdom);
_render_and_append(
{
user => $user,
domains => $subdoms,
global_config_data => $global_config_data,
}
);
_init_logs_for( $user, $subdom );
}
}
if ( keys %{ $domains_data->{addon_domains} } ) {
path($userconf)->append("\n#### addon domains for $user ##\n");
for my $aod ( sort keys %{ $domains_data->{addon_domains} } ) {
my $sub = $domains_data->{addon_domains}{$aod};
next unless $sub;
if ( !$subdomains_hash{$sub} ) {
warn "Skipping addon domain $aod for $user, corresponding sub domain $sub does not exist";
next;
}
my $ao_domains = [];
# subdomain first -- does not add mail subdomain
push @$ao_domains, $sub;
push( @$ao_domains, "www.$sub" ) unless Cpanel::WildcardDomain::is_wildcard_domain($sub);
# addon domain second -- adds mail subdomain
push @$ao_domains, $aod;
if ( !Cpanel::WildcardDomain::is_wildcard_domain($aod) ) {
push @$ao_domains, "www.$aod";
push( @$ao_domains, "mail.$aod" ) unless $subdomains_hash{"mail.$aod"};
}
_render_and_append(
{
user => $user,
domains => $ao_domains,
global_config_data => $global_config_data,
}
);
_init_logs_for( $user, $sub );
}
}
return 1;
}
our ( $server_tt, $docroots, $logging_hr, $caching_cache, $global_caching );
=head2 caching_global()
Return a hashref containing nginx's global caching settings. This sub will
return a cached result after the first call.
=head3 RETURNS
A hashref similar to the following:
{
'inactive_time' => '60m',
'proxy_cache_use_stale' => 'error timeout http_429 http_500 http_502 http_503 http_504',
'proxy_cache_valid' => {
'200 301 302' => '60m',
'404' => '1m'
},
'zone_size' => '10m',
'proxy_cache_lock' => 'off',
'proxy_cache_revalidate' => 'on',
'proxy_cache_background_update' => 'on',
'levels' => '1:2',
'enabled' => bless( do{\(my $o = 1)}, 'JSON::PP::Boolean' ),
'proxy_cache_min_uses' => 1,
'logging' => bless( do{\(my $o = 0)}, 'JSON::PP::Boolean' ),
};
=cut
sub caching_global {
return $global_caching if $global_caching;
my $caching_global = Cpanel::JSON::LoadFile($cache_file);
# UGMO :/ clean it up if the touch file exists and they have the defaults
# if they happen to edit it and make it the default and they really want that then they should not have this touchfile
if ( -e '/etc/nginx/ea-nginx/enable.micro-cache-defaults' ) {
# only change it if its the non-micro default
if ( keys %{ $caching_global->{proxy_cache_valid} } == 2
&& exists $caching_global->{proxy_cache_valid}{"200 301 302"}
&& $caching_global->{proxy_cache_valid}{"200 301 302"} eq "60m"
&& exists $caching_global->{proxy_cache_valid}{404}
&& $caching_global->{proxy_cache_valid}{404} eq "1m" ) {
warn "Changing default `proxy_cache_valid` to micro-cache version per the existance of the `/etc/nginx/ea-nginx/enable.micro-cache-defaults` touch-file\n";
delete $caching_global->{proxy_cache_valid}{"200 301 302"};
$caching_global->{proxy_cache_valid}{"301 302"} = "5m";
_write_json( $cache_file, $caching_global );
}
}
else {
# only change it back if its the micro default
if ( keys %{ $caching_global->{proxy_cache_valid} } == 2
&& exists $caching_global->{proxy_cache_valid}{"301 302"}
&& $caching_global->{proxy_cache_valid}{"301 302"} eq "5m"
&& exists $caching_global->{proxy_cache_valid}{404}
&& $caching_global->{proxy_cache_valid}{404} eq "1m" ) {
warn "Changing default `proxy_cache_valid` to non-micro-cache version per the absence of the `/etc/nginx/ea-nginx/enable.micro-cache-defaults` touch-file\n";
$caching_global->{proxy_cache_valid}{"200 301 302"} = "60m";
delete $caching_global->{proxy_cache_valid}{"301 302"};
_write_json( $cache_file, $caching_global );
}
}
return $caching_global;
}
=head2 _get_caching_hr()
Returns the given users cache settings. The cache settings have the following
order of precendence:
1. user specific based on '/var/cpanel/userdata/$user/nginx-cache.json'
2. global cache settings based on '/etc/nginx/ea-nginx/cache.json'
3. The caching defaults provided by caching_defaults()
NOTE: The results of this sub are cached for each user in the $caching_cache
hashref and the cached result is returned on subsequent calls to this sub.
=over
=item user
A valid cPanel username
=back
=head3 RETURNS
A hashref representing the user's caching configuration. See caching_global()
for a detailed example of what this data structure looks like.
=cut
sub _get_caching_hr {
my ($user) = @_;
if ( !exists $caching_cache->{$user} ) {
# at this point that file should exist; if it does not we want this to barf so we know about it
my $global_caching = caching_global();
my $user_caching = eval { Cpanel::JSON::LoadFile("/var/cpanel/userdata/$user/nginx-cache.json") } || {};
$caching_cache->{$user} = {
caching_defaults(),
%{$global_caching},
%{$user_caching},
};
}
return $caching_cache->{$user};
}
=head2 _get_logging_hr()
Return a hashref containing the user's global logging preferences. This sub
will return a cached result after the first call.
=head3 RETURNS
This returns a hashref similar to the following:
{
'default_format_name' => 'combined',
'enable_cache_log' => bless( do{\(my $o = 0)}, 'JSON::PP::Boolean' ),
'loglevel' => 'warn',
'piped_logs' => '1'
};
=cut
sub _get_logging_hr {
return $logging_hr if $logging_hr;
$logging_hr->{piped_logs} = Whostmgr::TweakSettings::get_value( Main => 'enable_piped_logs' );
my $wwwacct = Cpanel::Config::LoadWwwAcctConf::loadwwwacctconf() // {};
$logging_hr->{default_format_name} = $wwwacct->{LOGSTYLE} || "combined";
my $global_caching = caching_global();
$logging_hr->{enable_cache_log} = $global_caching->{logging};
if ( $logging_hr->{default_format_name} ne "combined" && $logging_hr->{default_format_name} ne "common" ) {
warn "Invalid “LOGSTYLE”, using `combined`\n";
$logging_hr->{default_format_name} = "combined";
}
my $e4c = Cpanel::EA4::Conf::Tiny::get_ea4_conf_hr();
$logging_hr->{loglevel} = $e4c->{loglevel} || 'warn';
return $logging_hr;
}
=head2 _get_group_for()
Returns the group name for the given username. The result is cached in a hash
and the cached result will be returned if the given username exists in the hash.
=over
=item user
A valid cPanel username
=back
=head3 RETURNS
A string representing the group name of the given username or undef.
=cut
my %group_cache;
sub _get_group_for {
my ($user) = @_;
return $group_cache{$user} if $group_cache{$user};
# prefer: group same name as user
$group_cache{$user} = scalar getgrnam($user) ? $user : undef;
return $group_cache{$user} if $group_cache{$user};
# otherwise: via gid
my $gid = _get_gid_for($user);
return unless $gid;
$group_cache{$user} = getgrgid($gid);
return $group_cache{$user};
}
=head2 _get_gid_for()
Returns the gid for the given username. The result is cached in a hash
and the cached result will be returned if the given username exists in the hash.
=over
=item user
A valid cPanel username
=back
=head3 RETURNS
An integer representing the group ID of the given username or undef.
=cut
my %gid_cache;
sub _get_gid_for {
my ($user) = @_;
return $gid_cache{$user} if $gid_cache{$user};
my $gid = ( getpwnam($user) )[3];
return unless $gid;
$gid_cache{$user} = $gid;
return $gid_cache{$user};
}
=head2 _get_domains_with_ssls()
Returns a hashref where the keys are the domains that have SSL enabled and the
value is 1 for every key.
=cut
our $domains_with_ssl;
sub _get_domains_with_ssls {
return $domains_with_ssl if $domains_with_ssl;
my %ssl_vhosts = map {
$_ =~ s/_SSL$//;
$_ => 1
} grep { substr( $_, -4 ) eq '_SSL' ? 1 : 0 } keys %{ _get_httpd_vhosts_hash() };
$domains_with_ssl = \%ssl_vhosts;
return $domains_with_ssl;
}
=head2 _get_httpd_vhosts_hash()
Returns a hashref containing the apache configuration structure for all the
vhosts that will be configured on the system. See
<Cpanel::ConfigFiles::Apache::Config> for more details regarding this data
structure. The result of this sub is cached and the cached result will be
returned after the first call.
=cut
our $httpd_vhosts_hr;
sub _get_httpd_vhosts_hash {
return unless ( eval { require Cpanel::ConfigFiles::Apache::Config; 1; } );
$httpd_vhosts_hr ||= Cpanel::ConfigFiles::Apache::Config::get_httpd_vhosts_hash();
return $httpd_vhosts_hr;
}
=head2 _is_standalone()
Returns whether nginx is currently in standalone mode or not. The return value
is cached and the cached value will be returned on subsequent calls to this sub.
=head3 RETURNS
Returns 1 or 0
=cut
our $standalone;
sub _is_standalone {
$standalone //= -e "/etc/nginx/ea-nginx/enable.standalone" ? 1 : 0;
return $standalone;
}
=head2 _render_and_append()
Appends the server block for the given domains to the given user's
configuration file. Dies if it fails to render the server block for the user.
=over
=item args
A hashref containing named args to be passed in.
=over 2
=item user
A valid cPanel username
=item domains
A arrayref of domains names
=item global_config_data
A hashref containing data that should be treated as read only that all users
can use to build their configs with.
=back
=back
=cut
sub _render_and_append {
my ($args) = @_;
my $user = $args->{user};
my $domains = $args->{domains};
my $global_config_data = $args->{global_config_data};
my $group = _get_group_for($user);
# at this point that file should exist; if it does not we want this to barf so we know about it
my $cpanel_password_protected_directories = Cpanel::JSON::LoadFile("/var/cpanel/userdata/$user/cpanel_password_protected_directories.json");
my $cpanel_redirects = Cpanel::JSON::LoadFile("/var/cpanel/userdata/$user/cpanel_redirects.json");
my $userconf = "/etc/nginx/conf.d/users/$user.conf";
$tt ||= Template->new( { INCLUDE_PATH => "/etc/nginx/" } );
$server_tt ||= path("/etc/nginx/ea-nginx/server.conf.tt")->slurp;
my $user_docroots = _get_docroots_for($user);
my $docroot = $user_docroots->{ $domains->[0] };
my $basic_auth = _get_basic_auth( $user, $docroot, $cpanel_password_protected_directories );
my $redirects = _get_redirects( $domains, $cpanel_redirects );
my $ssl_redirect = _get_ssl_redirect( $user => $domains );
my $secruleengineoff = -e "/etc/nginx/conf.d/modules/ngx_http_modsecurity_module.conf" ? _get_secruleengineoff( $user => $domains ) : 0;
my $passenger_apps = _get_passenger_apps( $user => $domains );
my $caching_hr = _get_caching_hr($user);
# Include numeric UID if they want it
my $uid = ( Cpanel::PwCache::getpwnam($user) )[2] if _wants_user_id();
my $fpm_socket;
my $fpm_socket_php_major_version;
# Check if the LSAPI flag file exists
my $lsapi_flag_file = "/etc/nginx/conf.d/modules/ea-nginx-lsapi-module.conf";
my $lsapi_available = -e $lsapi_flag_file;
my $lsapi_handler;
foreach my $dom (@$domains) {
my $has_fpm = Cpanel::PHPFPM::Get::get_php_fpm( $user, $dom );
if ($has_fpm) {
my $php_config_ref = Cpanel::PHP::Config::get_php_config_for_domains( [$dom] )->{$dom};
if ($php_config_ref) {
my ( $proxy, $socket ) = Cpanel::PHPFPM::Get::get_proxy_from_php_config_for_domain($php_config_ref);
$fpm_socket = $socket;
if ( $fpm_socket =~ m{/\w+-php(\d)\d/} ) {
$fpm_socket_php_major_version = $1;
}
}
elsif ( $global_config_data->{standalone} ) {
warn "Could not find PHP configuration for “$dom”, it will not be configured to use PHP-FPM\n";
}
last;
} elsif ($lsapi_available) {
my $php_config_ref;
eval {
$php_config_ref = Cpanel::PHP::Config::get_php_config_for_domains( [$dom] )->{$dom};
};
# Skip to next domain if there was an error or if the phpversion is not set
next unless $php_config_ref && $php_config_ref->{'phpversion'};
my $php = Cpanel::ProgLang->new(type => 'php');
my $conf = Cpanel::ProgLang::Conf->new(type => $php->type());
my $php_handler = $conf->get_package_info(package => $php_config_ref->{'phpversion'});
# go next if domain's handler is not lsapi
next unless $php_handler eq 'lsapi';
# Construct the lsapi_handler variable
$lsapi_handler = "application/x-httpd-" . $php_config_ref->{'phpversion'} . "___lsphp";
last;
}
}
$domains_with_ssl ||= _get_domains_with_ssls();
my @service_subdomains;
$httpd_vhosts_hr ||= _get_httpd_vhosts_hash();
if ( exists $httpd_vhosts_hr->{"$domains->[0]_SSL"}{proxy_subdomains} ) {
for my $sub ( sort keys %{ $httpd_vhosts_hr->{"$domains->[0]_SSL"}{proxy_subdomains} } ) {
push @service_subdomains, map { "$sub.$_" } @{ $httpd_vhosts_hr->{"$domains->[0]_SSL"}{proxy_subdomains}{$sub} };
}
}
my $combined_tls = Cpanel::Apache::TLS->get_tls_path( $domains->[0] );
if ( !_is_certfile_valid($combined_tls) ) {
if ( -f "/var/cpanel/ssl/cpanel/mycpanel.pem" ) {
$combined_tls = "/var/cpanel/ssl/cpanel/mycpanel.pem";
}
else {
$combined_tls = "/var/cpanel/ssl/cpanel/cpanel.pem";
}
}
my $ip = Cpanel::DomainIp::getdomainip( $domains->[0] ) || '127.0.0.1'; # juuust in case so we don’t break nginx w/ syntax error
my $servername_ip = $ip ne '127.0.0.1' && $global_config_data->{domain_ips}{$ip} && $global_config_data->{domain_ips}{$ip} eq $domains->[0] ? $ip : "";
# Use the public IP on NAT systems
# NOTE: this is fine to run on non-NAT systems as it just returns the same IP
$servername_ip = Cpanel::NAT::get_public_ip($servername_ip) if $servername_ip;
my $wordpress_hr = _get_wordpress_info( $user, $user_docroots->{ $domains->[0] } );
my $has_wordpress = ( $wordpress_hr->{docroot_install} || scalar @{ $wordpress_hr->{non_docroot_uris} } ) ? 1 : 0;
$tt->process(
\$server_tt,
{
docroot => $docroot,
ssl_certificate => $combined_tls,
ssl_certificate_key => $combined_tls,
ssl_redirect => $ssl_redirect,
secruleengineoff => $secruleengineoff,
domains => $domains,
service_subdomains => \@service_subdomains,
user => $user,
group => $group,
( defined $uid ? ( uid => $uid ) : () ),
proxy_ip => $ip,
ip => $servername_ip,
ipv6 => $global_config_data->{ipv6},
http2 => $global_config_data->{http2},
fpm_socket => $fpm_socket,
fpm_socket_php_major_version => $fpm_socket_php_major_version,
lsapi_handler => $lsapi_handler,
has_wordpress => $has_wordpress,
wordpress => {
docroot_install => $wordpress_hr->{docroot_install},
non_docroot_uris => $wordpress_hr->{non_docroot_uris},
},
basic_auth => $basic_auth,
redirects => $redirects,
logging => $global_config_data->{logging},
ea4conf => $global_config_data->{e4c},
settings => $global_config_data->{cur_settings},
passenger => {
apps => $passenger_apps,
},
behavior => {
standalone => $global_config_data->{standalone},
caching => $caching_hr,
},
proxysubdomains_enabled => $global_config_data->{cpconf}->{proxysubdomains},
},
sub {
my ($out) = @_;
path($userconf)->append($out);
return 1;
}
);
if ( $tt->error() ) {
unlink $userconf;
die $tt->error();
}
return 1;
}
=head2 _init_logs_for()
Initializes the logs for a given user and domain.
=over
=item user
A valid cPanel username
=item domain
The domain to initialize
=back
=cut
sub _init_logs_for {
my ( $user, $domain ) = @_;
my $gid = _get_gid_for($user);
# STD log
Cpanel::FileUtils::TouchFile::touchfile("/var/log/nginx/domains/$domain");
chmod 0640, "/var/log/nginx/domains/$domain";
chown 0, $gid, "/var/log/nginx/domains/$domain";
# SSL log
Cpanel::FileUtils::TouchFile::touchfile("/var/log/nginx/domains/$domain-ssl_log");
chmod 0640, "/var/log/nginx/domains/$domain-ssl_log";
chown 0, $gid, "/var/log/nginx/domains/$domain-ssl_log";
# Bytes log
Cpanel::FileUtils::TouchFile::touchfile("/var/log/nginx/domains/$domain-bytes_log");
chmod 0644, "/var/log/nginx/domains/$domain-bytes_log";
chown 0, 0, "/var/log/nginx/domains/$domain-bytes_log";
return;
}
=head2 _is_certfile_valid()
Tests if an SSL certificate file is valid
=head3 RETURNS
Returns false if the certificate file has been found to be invalid
Returns true otherwise
=cut
sub _is_certfile_valid {
my ($certfile) = @_;
# It cannot be valid if it is absent or empty
if ( !-f $certfile || -z _ ) {
print "Certificate file $certfile is absent or empty; excluding from config file\n";
return;
}
# We cannot meaningfully test further if the openssl binary hasn't been found
# This issue will have already been reported
my $openssl = _get_openssl_obj();
return 1 unless $openssl;
my $output = $openssl->get_cert_text( { crtfile => $certfile } );
if ( $output->{CHILD_ERROR} ) {
print "Certificate file $certfile is invalid; excluding from config file: " . $output->{stderr} . "\n";
return;
}
return 1;
}
=head2 _get_openssl_obj()
Gets a cached instance of a Cpanel::OpenSSL object
If not present in the cache, then allocates & caches
=head3 RETURNS
Returns a Cpanel::OpenSSL instance if OpenSSL binaries are present
Returns false otherwise
=cut
sub _get_openssl_obj {
$openssl_obj //= Cpanel::OpenSSL->new();
if ( !$openssl_obj ) {
warn "The openssl binary could not be located\n";
}
return $openssl_obj;
}
=head2 _get_homedir()
Returns the path to the given cPanel user's homedir
=head3 RETURNS
The path to the user's homedir
=cut
sub _get_homedir {
my ($user) = @_;
return ( getpwnam($user) )[7];
}
=head2 _get_passenger_apps()
Given a user and an array reference of domains, it consumes the applications
userdatafile for the user, and returns an array reference of hashes for any
applications that are enabled for one of the given domains.
=over
=item user
A cPanel username
=item domains
A arrayreferences of domains owned by the user
=back
=head3 RETURNS
An array reference of hashes containing application data for any enabled
passenger applications
=cut
sub _get_passenger_apps {
my ( $user, $domains ) = @_;
my @apps;
my %domains;
@domains{ @{$domains} } = ();
my $apps_hr = eval { Cpanel::JSON::LoadFile("/var/cpanel/userdata/$user/applications.json") } || {};
for my $app_name ( sort keys %{$apps_hr} ) {
my $app = $apps_hr->{$app_name};
if ( $app->{enabled} && exists $domains{ $app->{domain} } ) {
_get_application_paths($app);
push @apps, $app;
}
}
return \@apps;
}
=head2 _get_userdata_for()
Given a cPanel username and a domain that it owns, it returns the userdata for
the domain.
=over
=item user
A valid cPanel username
=item dom
A domain owned by the cPanel user
=back
=head3 RETURNS
A hashref representing the userdata for the user's domain
=cut
our %load_userdata_cache;
sub _get_userdata_for {
my ( $user, $dom ) = @_;
if ( !exists $load_userdata_cache{$user}{$dom} ) {
$load_userdata_cache{$user}{$dom} = Cpanel::Config::userdata::Load::load_userdata( $user => $dom ) || {};
}
return $load_userdata_cache{$user}{$dom};
}
=head2 _get_ssl_redirect()
Given a cPanel username and an arrayref of domains that the cpanel user owns,
return 1 if any of the domains has force ssl redirect enabled. Otherwise,
return undef
=over
=item user
A valid cPanel username
=item domains
An arrayref of domain names
=back
=head3 RETURNS
1 or undef
=cut
sub _get_ssl_redirect {
my ( $user, $domains ) = @_;
for my $dom ( @{$domains} ) {
my $lu = _get_userdata_for( $user => $dom );
return 1 if $lu->{ssl_redirect};
}
return;
}
=head2 _get_secruleengineoff()
Returns 1 if mod_security is enabled for the given user. Otherwise, returns
undef.
=over
=item user
A valid cPanel username
=item domains
An arrayref of domains
=back
=head3 RETURNS
1 or undef
=cut
sub _get_secruleengineoff {
my ( $user, $domains ) = @_;
for my $dom ( @{$domains} ) {
my $lu = _get_userdata_for( $user => $dom );
return 1 if $lu->{secruleengineoff};
}
return;
}
=head2 _get_redirects()
Given an arrayref of domains and an arrayref of hashes describing redirect
userdata for a cPanel user, it returns an arrayref of hashes describing the
redirects for each domain.
=over
=item domains
An arrayref of domains
=item cpanel_redirects
An arrayref of hashes describing redirect userdata similar to the following:
[
{
docroot => '/home/foo/public_html',
domain => '.*',
kind => 'rewrite',
matchwww => 1,
opts => 'L',
sourceurl => '/uri',
targeturl => 'https://domain.tld',
type => 'permanent',
wildcard => 0,
},
]
=back
=head3 RETURNS
An arrayref of hashes describing the redirects that need to be configured that
will appear similar to the following:
[
{
flag => 'redirect',
replacement => 'https://domain.tld/$1',
regex => '^\\/uri\\/?(.*)$',
},
{
flag => 'permanent',
replacement => 'https://domain.tld',
regex => '^\\/uri$',
},
]
=cut
sub _get_redirects {
my ( $domains, $cpanel_redirects ) = @_;
my %domains;
@domains{ @{$domains} } = ();
my @applicable_redirects;
for my $redirect ( @{$cpanel_redirects} ) {
next unless exists $domains{ $redirect->{domain} } || $redirect->{domain} eq '.*';
# Ensure that $redirect->{domain} correctly references the domain
my $redirect_domain = $redirect->{'domain'};
# Skip redirects for domains that are proxied
if (exists $apache2nginx_proxied_domains{$redirect_domain}) {
warn "Excluding redirects for proxied domain '$redirect_domain'.\n";
next;
}
if ( $redirect->{domain} ne '.*' ) {
if ( $redirect->{targeturl} =~ m{^(?:[A-Za-z0-9\+_\.\-\:]+)?//(?:www\.)?\Q$redirect->{domain}\E(?:$|/|\?|\#)} ) {
warn "Skipping circular redirect for “$redirect->{domain}” to “$redirect->{targeturl}”\n";
next;
}
}
my %res;
if ( $redirect->{statuscode} eq "301" ) {
$res{flag} = "permanent";
}
elsif ( $redirect->{statuscode} eq "302" ) {
$res{flag} = "redirect";
}
else {
warn "Skipping non 301/302 redirect\n";
next;
}
# sourceurl and targeturl can be anything meaning a user would take down the server (not just their site) if its invalid
# or worse they could inject configuration into nginx. We need to ensure nginx sees them as a single string.
# Cpanel::UTF8::Utils::quotemeta() make sense for sourceurl since its a regex
# 78 does not have Cpanel::UTF8::Utils so we use String::UnicodeUTF8 which it does have.
# Escaping is bad for targeturl because the slashes are literal despite it being syntactically correct ¯\_(ツ)_/¯
if ( !Cpanel::Validate::URL::is_valid_url( $redirect->{targeturl} ) ) {
warn "Skipping invalid targeturl “$redirect->{targeturl}”\n";
next;
}
if ( $redirect->{wildcard} ) {
$res{regex} = $redirect->{sourceurl};
$res{regex} =~ s{/+$}{};
$res{regex} = '^' . String::UnicodeUTF8::quotemeta_bytes( $res{regex} ) . '\\/?(.*)$';
$res{replacement} = $redirect->{targeturl} . '$1';
}
else {
$res{regex} = '^' . String::UnicodeUTF8::quotemeta_bytes( $redirect->{sourceurl} ) . '$';
$res{replacement} = $redirect->{targeturl};
}
push @applicable_redirects, \%res;
}
return \@applicable_redirects;
}
=head2 _get_basic_auth()
Returns a hashref containing directory privacy information for the domain whose
docroot is being processed.
=over
=item user
A valid cPanel username
=item docroot
The docroot for the domain that is being processed
=item cpanel_password_protected_directories
A hashref of directory privacy userdata similar to the following:
{
'/public_html' => {
'_htaccess_mtime' => 1234,
'realm_name' => 'docroot',
},
'/public_html/dir' => {
'_htaccess_mtime' => 5678,
'realm_name' => 'dir',
},
}
=back
=head3 RETURNS
A hashref of directory privacy information similar to the following:
{
realm_name => 'docroot',
auth_file => '/home/foo/.htpasswds/public_html/passwd',
_htaccess_mtime => 1234,
locations => {
'/sub' => {
'auth_file' => '/home/foo/.htpasswds/public_html/sub/passwd',
'realm_name' => 'sub',
},
},
}
=cut
sub _get_basic_auth {
my ( $user, $docroot, $cpanel_password_protected_directories ) = @_;
my $docroot_rel = $docroot;
my $homedir = _get_homedir($user);
$docroot_rel =~ s{\Q$homedir\E}{};
my $basic_auth = $cpanel_password_protected_directories->{$docroot_rel};
my $auth_file_from;
if ( !$basic_auth ) {
my $docroot_rel_copy = $docroot_rel;
while ($docroot_rel_copy) {
$docroot_rel_copy =~ s{/[^/]+$}{};
if ( exists $cpanel_password_protected_directories->{$docroot_rel_copy} ) {
$basic_auth = $cpanel_password_protected_directories->{$docroot_rel_copy};
$auth_file_from = $docroot_rel_copy;
last;
}
}
}
$basic_auth ||= $cpanel_password_protected_directories->{""} || undef;
if ($basic_auth) {
$basic_auth->{auth_file} =
$auth_file_from ? "$homedir/.htpasswds$auth_file_from/passwd"
: $cpanel_password_protected_directories->{$docroot_rel} ? "$homedir/.htpasswds$docroot_rel/passwd"
: "$homedir/.htpasswds/passwd";
}
$basic_auth->{locations} = {};
for my $dir ( keys %{$cpanel_password_protected_directories} ) {
my $abs = "$homedir$dir";
if ( $abs =~ m{^\Q$docroot\E(/.+)$} ) {
my $loc = $1;
$basic_auth->{locations}{$loc} = {
auth_file => "$homedir/.htpasswds$docroot_rel$loc/passwd",
realm_name => $cpanel_password_protected_directories->{$dir}{realm_name},
};
}
}
return $basic_auth;
}
my $wordpress_lu;
sub _get_wp_uapi {
my ($user) = @_;
my $uapi = Cpanel::SafeRun::Object->new(
program => "/usr/bin/uapi",
args => [ "--output=json", "--user=$user", "WordPressInstanceManager", "get_instances" ],
);
warn "Could not determine managed wordpress instances for $user\n" if $uapi->CHILD_ERROR();
return eval { Cpanel::JSON::Load( $uapi->stdout() ) } || {};
}
my $wp_toolkit_list;
sub _get_wp_toolkit_list {
print "Gathering wordpress data...\n";
my $run = Cpanel::SafeRun::Object->new(
program => $wp_toolkit_bin,
args => [ '--list', '-format', 'json' ],
);
warn 'Could not determine managed wordpress instances via wordpress toolkit' if $run->CHILD_ERROR();
print "...done gathering wordpress data\n";
return eval { Cpanel::JSON::Load( $run->stdout() ) } || [];
}
sub _get_wp_toolkit_list_for_user {
my ( $user, $docroot, $res ) = @_;
$wp_toolkit_list ||= _get_wp_toolkit_list();
my ( @list, %non_docroot_uris );
foreach my $wordpress_instance (@$wp_toolkit_list) {
if ( $wordpress_instance->{fullPath} =~ m{/\Q$user\E/} ) {
if ( $wordpress_instance->{fullPath} eq $docroot ) {
$res->{docroot_install} = 1;
next;
}
my $uri_from_path = $wordpress_instance->{fullPath};
$uri_from_path =~ s{^\Q$docroot\E/}{};
my $uri_from_siteurl = $wordpress_instance->{siteUrl};
$uri_from_siteurl =~ s{^https?://[^/]+/}{};
if ( $uri_from_path eq $uri_from_siteurl ) {
$non_docroot_uris{$uri_from_path} = 1;
next;
}
}
push @list, $wordpress_instance;
}
# Do it this way in case there are duplicate entries in $wp_toolkit_list
@{ $res->{non_docroot_uris} } = keys %non_docroot_uris;
# Remove the entries in $res from the list since they will only match 1 time
$wp_toolkit_list = \@list;
return $res;
}
=head2 _get_wordpress_info()
Given a user and a docroot, it gathers information the locations of wordpress
installs relative to the document root and returns a hashref with this info.
=over
=item user
A cPanel username
=item docroot
The path to the user's docroot
=back
=head3 RETURNS
A hashref that appears similar to the following:
{
docroot_install => 1,
non_docroot_uris => [
'foo',
'bar',
],
}
=cut
our %wordpress_info;
sub _get_wordpress_info {
my ( $user, $docroot ) = @_;
return $wordpress_info{$docroot} if exists $wordpress_info{$docroot};
my $docroot_underscore = $docroot;
$docroot_underscore =~ s{/}{_}g;
my $cache_file = "$wordpress_cache_dir/${user}_${docroot_underscore}_wordpress_info.json";
# Returns undef if the cache_file fails to load or does not contain the required keys
$wordpress_info{$docroot} = _get_wordpress_info_from_cache($cache_file) if _is_wordpress_info_cache_valid($cache_file);
return $wordpress_info{$docroot} if exists $wordpress_info{$docroot};
my $res = {
docroot_install => 0,
non_docroot_uris => [],
};
if ( -e $wp_toolkit_bin ) {
$wordpress_info{$docroot} = _get_wp_toolkit_list_for_user( $user, $docroot, $res );
}
elsif ( -e '/usr/local/cpanel/Cpanel/API/WordPressInstanceManager.pm' ) {
$wordpress_lu->{$user} ||= _get_wp_uapi($user);
$wordpress_info{$docroot} = _get_wordpress_info_from_wpmanager( $docroot, $wordpress_lu->{$user}{result}{data}{instances}, $res );
}
else {
$wordpress_info{$docroot} = $res;
}
# cache wordpress info
_ensure_wordpress_info_cache_directory();
_write_json( $cache_file, $res );
return $wordpress_info{$docroot};
}
sub _get_wordpress_info_from_wpmanager {
my ( $docroot, $wordpress_instances, $res ) = @_;
# paths passed in and from API call do not have trailing slash, if that changes we could normalize them
for my $wp_instance (@$wordpress_instances) {
if ( length $wp_instance->{rel_path} ) {
my $instance_docroot = $wp_instance->{full_path};
$instance_docroot =~ s{/\Q$wp_instance->{rel_path}\E$}{};
if ( $instance_docroot eq $docroot ) {
push @{ $res->{non_docroot_uris} }, $wp_instance->{rel_path};
}
}
else {
if ( $wp_instance->{full_path} eq $docroot ) {
$res->{docroot_install} = 1;
}
}
}
return $res;
}
sub _ensure_wordpress_info_cache_directory {
mkdir $wordpress_cache_dir;
return;
}
=head2 _is_wordpress_info_cache_valid()
Returns 1 or 0 depending on if the cache file is valid or not. NOTE: it will
always return 0 if the '/var/cpanel/nginx_disable_wordpress_info_cache' touch
file is in place.
=over
=item cache_file
The path of the cache file to check
=back
=cut
sub _is_wordpress_info_cache_valid {
my ($cache_file) = @_;
return 0 if -e $disable_wordpress_info_cache_file;
my $now = time();
my $ttl = 18000; # Cache it for 5 days
my $mtime = ( stat($cache_file) )[9];
return ( -s $cache_file && $mtime > ( $now - $ttl ) ) ? 1 : 0;
}
=head2 _get_wordpress_info_from_cache()
Loads the given cache file, and returns the data as a hashref. It will warn and
return undef if the given cache file is not valid json or if it is missing the
required keys.
=over
=item cache_file
The path to the cache file to load
=back
=cut
sub _get_wordpress_info_from_cache {
my ($cache_file) = @_;
my $res;
unless ( eval { $res = Cpanel::JSON::LoadFile($cache_file); 1; } ) {
warn "Failed to load cache file: ‘$cache_file’\n";
return;
}
if ( !exists $res->{docroot_install} || !exists $res->{non_docroot_uris} ) {
warn "The cache file ‘$cache_file’ has missing data\n";
return;
}
return $res;
}
=head2 _validate_user_arg()
Validate that the given username is a valid cPanel user
=over
=item user
A cPanel user to validate
=back
=head3 RETURNS
Returns 1 if the give username is valid. Dies otherwise.
=cut
sub _validate_user_arg {
my ( $app, $user ) = @_;
_bail( $app, "The user argument is missing." ) if !$user;
my $user_lookup = _get_user_domains();
_bail( $app, "The given user is not a cPanel user.\n" ) if !$user_lookup->{$user};
return 1;
}
sub _get_cmd {
return $cmds;
}
=head2 _bail()
Handles unclean script exits. For example, if an invalid username is given.
=cut
sub _bail {
my ( $app, $msg ) = @_;
chomp($msg);
die "$msg\n" if $ENV{ __PACKAGE__ . "::bail_die" }; # for API calls, otherwise:
warn "$msg\n";
$app->help();
exit(1); # there is no return()ing from this lol
}
sub _delete_glob {
my ($glob) = @_;
for my $item ( File::Glob::csh_glob($glob) ) {
# File::Path::Tiny::rm does not delete files
if ( -l $item || -f _ ) {
unlink($item);
}
elsif ( -d $item ) {
File::Path::Tiny::rm($item);
}
}
return;
}
=head2 _get_cpconf_hr()
Returns a hashref containing the cPanel settings from
'/var/cpanel/cpanel.config'. The result of this sub is cached and the cached
result will be returned after the first call.
=cut
our $cpconf_hr;
sub _get_cpconf_hr {
$cpconf_hr ||= Cpanel::Config::LoadCpConf::loadcpconf();
return $cpconf_hr;
}
=head2 _get_nginx_bin()
Returns the path to the nginx binary. Dies if the nginx does not exist or is
not executable.
=cut
sub _get_nginx_bin {
my $nginx_bin = '/usr/sbin/nginx';
# This executable is provided by the ea-nginx RPM so this should never happen
die "Could not find an executable nginx binary\n" unless -x $nginx_bin;
return $nginx_bin;
}
=head2 _attempt_to_fix_syntax_errors()
Parses the given output for known nginx configuration syntax errors such as
duplicate keys and attempts to fix them. Returns 0 or 1 depending on if it
resolved an issue or not.
=over
=item combined_output
A multiline string to parse
=back
=head3 RETURNS
Returns 1 if it resolved an issue. Returns 0 otherwise.
=cut
sub _attempt_to_fix_syntax_errors {
my ($combined_output) = @_;
die "_attempt_to_fix_syntax_errors: ‘combined_output’ arg is required\n"
unless defined $combined_output;
my $resolved_issue = 0;
my @output_lines = split /\n/, $combined_output;
foreach my $output_line (@output_lines) {
# For example:
# nginx: [emerg] "client_max_body_size" directive is duplicate in /etc/nginx/conf.d/ea-nginx.conf:19
# use non greedy modifier here to ensure we do not match more than we intend to
if ( $output_line =~ m{^nginx: \[emerg\] "(.*?)" directive is duplicate in (.*?):([0-9]+)$} ) {
my $key = $1;
my $file_path = $2;
my $line_num = $3;
die "‘$file_path’ reported an error on line $line_num. However, ‘$file_path’ does not exist on the system\n"
unless -s $file_path;
my $file = Path::Tiny::path($file_path);
my @file_lines = $file->lines();
chomp(@file_lines);
$line_num--; # avoid off by 1 error (line 42 in the file is item 41 in the array)
# For example:
# nginx_key value_for_key;
if ( $file_lines[$line_num] =~ m{^\s*\Q$key\E.*;$} ) {
$resolved_issue = 1;
$file_lines[$line_num] = '# ' . $file_lines[$line_num];
$line_num++;
print "Line $line_num in the file ‘$file_path’ was commented out since another entry exists for the key: $key\n";
}
$file->spew( join( "\n", @file_lines ) );
last; # nginx -t only reveals the first issue it encounters
}
elsif ( $output_line =~ m{^nginx: \[emerg\].*?in (/etc/nginx/conf.d/users/[a-z0-9]{1,16}\.conf):([0-9]+)$} ) {
my $file_path = $1;
my $line_num = $2;
die "‘$file_path’ reported an error on line $line_num. However, ‘$file_path’ does not exist on the system\n"
unless -s $file_path;
unlink $file_path;
die "Unable to remove $file_path\n" if -f $file_path;
$resolved_issue = 1;
print "‘$file_path’ was removed due to a configuration issue: $output_line\n";
last; # nginx -t only reveals the first issue it encounters
}
}
return $resolved_issue;
}
=head2 _get_num_total_domains()
Returns an integer representing the total number of domains hosted on the
system.
=cut
sub _get_num_total_domains {
return unless ( eval { require Cpanel::Config::LoadUserDomains::Count; 1; } );
return Cpanel::Config::LoadUserDomains::Count::countuserdomains();
}
=head2 _get_user_domains()
This is a wrapper around 'Cpanel::Config::LoadUserDomains::loaduserdomains'. See
the POD for loaduserdomains for details. The result of this sub is cached and
the cached result will be returned after the first call.
=head3 RETURNS
This function returns a hash ref with a key/value mapping of users to domains.
=cut
my $user_domains;
sub _get_user_domains {
$user_domains //= Cpanel::Config::LoadUserDomains::loaduserdomains( undef, 0, 1 );
return $user_domains;
}
=head2 _rebuild_and_restart_apache()
Rebuild the apache configuration and restart the service
=cut
sub _rebuild_and_restart_apache {
my $run = Cpanel::SafeRun::Object->new(
program => '/usr/local/cpanel/scripts/rebuildhttpdconf',
args => [],
);
if ( $run->CHILD_ERROR() ) {
my $out = $run->stdout() . $run->stderr();
warn "Failed to rebuild apache configuration: $out\n";
}
Cpanel::HttpUtils::ApRestart::BgSafe::restart();
return;
}
=head2 _update_user_configs_in_parallel_mode()
Build the configuration for all users asynchronously.
=over
=item global_config_data
A hashref containing data that should be treated as read only that all users
can use to build their configs with.
=back
=head3 RETURNS
A hashref of users that had errors during the configuration process that should
be similar to the following:
{
user1 => 'error message',
user2 => 'another error message',
}
OR undef if all of the users were configured successfully.
=cut
sub _update_user_configs_in_parallel_mode {
my ($global_config_data) = @_;
my $user_lookup = _get_user_domains();
my %errors = mce_loop {
my ( $mce, $chunk_ref ) = @_;
my $err_href = _process_users( $chunk_ref, $global_config_data );
MCE->gather( $_, $err_href->{$_} ) for ( keys %$err_href );
}
keys %$user_lookup;
return \%errors;
}
=head2 _update_user_configs_in_serial_mode()
Build the configuration for all users synchronously.
=over
=item global_config_data
A hashref containing data that should be treated as read only that all users
can use to build their configs with.
=back
=head3 RETURNS
A hashref of users that had errors during the configuration process that should
be similar to the following:
{
user1 => 'error message',
user2 => 'another error message',
}
OR undef if all of the users were configured successfully.
=cut
sub _update_user_configs_in_serial_mode {
my ($global_config_data) = @_;
print "Serial mode detected. User configuration will take longer …\n";
my $user_lookup = _get_user_domains();
my @users = sort keys %$user_lookup;
my $errors = _process_users( \@users, $global_config_data );
return $errors;
}
=head2 _process_users()
Accepts an arrayref of users and writes the configuration for each user.
=over
=item users
A arrayref of users to process
=item global_config_data
A hashref containing data that should be treated as read only that all users
can use to build their configs with.
=back
=head3 RETURNS
A hashref of users to error messages for any user that configuration fails for.
=cut
sub _process_users {
my ( $users, $global_config_data ) = @_;
my %errors;
foreach my $usr (@$users) {
eval { _write_user_conf( $usr, $global_config_data ); };
$errors{$usr} = $@ if $@;
}
return \%errors;
}
=head2 _update_for_custom_configs()
Should only be called by run(). The idea here is to allow clients to place a
file in the $custom_settings_dir prior to installing the ea-nginx RPM. Then,
this script will pick up the file and place it in the proper location on first
run. After which, these subs should become no-ops unless another overwrite
file is place in the $custom_settings_dir. Currently, the following two files
are supported:
settings.json
cache.json
To add a new file, create a sub to handle moving said file to its proper
location, then call the sub from _update_for_custom_configs()
=cut
sub _update_for_custom_configs {
_update_nginx_settings_config_file();
_update_nginx_cache_config_file();
return;
}
=head2 _update_nginx_settings_config_file()
This reads in a settings.json file from the $custom_settings_dir and moves it
to '/etc/nginx/ea-nginx/settings.json'. NOTE: the following keys are not
honored since they are server specific:
apache_port
apache_port_ip
apache_ssl_port
apache_ssl_port_ip
=cut
sub _update_nginx_settings_config_file {
my $custom_settings_file = "$custom_settings_dir/settings.json";
my $custom_settings_path = path($custom_settings_file);
if ( $custom_settings_path->exists ) {
# do not allow specifying apache_ ones
my $new = Cpanel::JSON::LoadFile($custom_settings_file);
my $cur = _get_settings_hr();
$settings_hr = undef; # clear _get_settings_hr() cache since we are changing settings
for my $key (qw(apache_port apache_port_ip apache_ssl_port apache_ssl_port_ip)) {
$new->{$key} = $cur->{$key};
}
_write_json( $custom_settings_file, $new );
# Do not use Path::Tiny until 110 or greater only is supported
# See CPANEL-41646 for details
# $custom_settings_path->move($settings_file);
Cpanel::FileUtils::Move::safemv( '-fv', $custom_settings_file, $settings_file );
}
return;
}
=head2 _update_nginx_cache_config_file()
This reads in a cache.json file from the $custom_settings_dir and moves it to
'/etc/nginx/ea-nginx/cache.json'.
=cut
sub _update_nginx_cache_config_file {
my $custom_cache_file = "$custom_settings_dir/cache.json";
my $custom_cache_path = path($custom_cache_file);
# Do not use Path::Tiny until 110 or greater only is supported
# See CPANEL-41646 for details
# $custom_cache_path->move($cache_file) if $custom_cache_path->exists;
Cpanel::FileUtils::Move::safemv( '-fv', $custom_cache_file, $cache_file ) if $custom_cache_path->exists;
return;
}
=head2 _get_global_config_data()
Gathers data that should be treated as read only and will be the same for every
user when rendering the user's config file.
This sub will get called directly in config() so that the data it gathers is
cloned to every fork and thus will be the same in every fork when building in
parallel mode (MCE::Loop).
=head3 RETURNS
A hashref containing data that should be considered read only
=cut
sub _get_global_config_data {
my %global_config_data = (
domain_ips => _get_domain_ips(),
ipv6 => _has_ipv6(),
http2 => _wants_http2(),
logging => _get_logging_hr(),
e4c => Cpanel::EA4::Conf->instance->as_hr(),
cur_settings => _get_settings_hr(),
standalone => _is_standalone(),
cpconf => _get_cpconf_hr(),
);
return \%global_config_data;
}
=head2 _populate_wordpress_cache( { all => 1 } )
This populates the wordpress cache before the script starts forking so that a
potentially expensive call to wp-toolkit will only ever be called a maximum of
one time
=over
=item args
A hashref containing the known args as listed below
=over
=item all => 1
This tells the sub to populate the wordpress cache for all users
=item user => $user
This tells the sub to only populate the wordpress cache for the given user
=back
=back
=cut
sub _populate_wordpress_cache {
my ($args) = @_;
die "_populate_wordpress_cache: invalid args passed. Either ‘all’ or ‘user’ is required\n"
if ( !$args->{all} && !$args->{user} );
return _populate_wordpress_cache_for_all_users() if $args->{all};
return _populate_wordpress_cache_for_user( $args->{user} ) if $args->{user};
return;
}
sub _populate_wordpress_cache_for_all_users {
my $user_lookup = _get_user_domains();
foreach my $user ( sort keys %$user_lookup ) {
_populate_wordpress_cache_for_user($user);
}
return;
}
sub _populate_wordpress_cache_for_user {
my ($user) = @_;
my $docroots = _get_docroots_for($user);
foreach my $domain ( keys %$docroots ) {
_get_wordpress_info( $user, $docroots->{$domain} );
}
return;
}
=head2 _get_docroots_for($user)
=over
=item user
A valid cPanel username
=back
=head3 RETURNS
A hashref where the keys are all the domains owned by the given user and the
values represent the docroot for the given domain
=cut
sub _get_docroots_for {
my ($user) = @_;
$docroots->{$user} ||= { Cpanel::DomainLookup::DocRoot::getdocroots($user) };
return $docroots->{$user};
}
=head2 _get_domain_ips()
Maps IPs to the primary domain that they are associated with on the server.
This allows us to configure the domain on only one server block and to the
server block that the user desires according to metadata / apache.
=head3 RETURNS
A hashref that maps IPs to domains.
=cut
sub _get_domain_ips {
my $primary_vhosts_obj = Cpanel::HttpUtils::Vhosts::PrimaryReader->new();
my %domain_ips;
my $user_lookup = _get_user_domains();
for my $usr ( sort keys %$user_lookup ) {
my $domain = Cpanel::Config::userdata::Load::load_userdata_main($usr)->{main_domain};
my $ip = Cpanel::DomainIp::getdomainip($domain);
if ( $ip && !exists $domain_ips{$ip} ) {
if ( Cpanel::DIp::IsDedicated::isdedicatedip($ip) || $domain eq $primary_vhosts_obj->get_primary_non_ssl_servername($ip) ) {
$domain_ips{$ip} = $domain;
}
}
}
return \%domain_ips;
}
=head2 _wants_user_id()
Returns whether the server admin wants the user id of the server block that was
hit logged or not. Caches the result for subsequent calls.
=head3 RETURNS
Returns 1 or 0 depending on the existence of the touch file
=cut
our $wants_user_id;
sub _wants_user_id {
return $wants_user_id if defined $wants_user_id;
$wants_user_id = -e $user_id_file ? 1 : 0;
return $wants_user_id;
}
1;
Zerion Mini Shell 1.0