26 Commits

Author SHA1 Message Date
Alexey Berezhok
f99c8f3b31 Fixed template 2026-08-12 15:43:39 +03:00
Alexey Berezhok
5914f54e64 Fixes for exim and divecot work 2026-08-10 21:41:37 +03:00
Alexey Berezhok
b31dce3817 Some fixes in documentaion 2026-06-04 14:23:42 +03:00
Alexey Berezhok
09aa301dd1 - Синхронизация с github
- Добавлены изменения в доку от hestiacn
2026-06-04 14:11:27 +03:00
Alexey Berezhok
317e3e215e Fixes 2026-05-23 22:45:01 +03:00
Alexey Berezhok
4f999094f3 Fixes 2026-05-20 23:24:30 +03:00
Alexey Berezhok
5e32af0148 Fixes 2026-05-19 23:04:15 +03:00
Alexey Berezhok
6ff9d67909 Fixes 2026-05-16 00:10:12 +03:00
Alexey Berezhok
af9e6bfb82 Fixes 2026-05-15 00:34:14 +03:00
Alexey Berezhok
e0419fea4b Fixes 2026-05-14 00:32:06 +03:00
Alexey Berezhok
3cc428df43 Fixes 2026-05-12 01:27:01 +03:00
Alexey Berezhok
9d1afcdf28 Fixes 2026-05-11 23:28:47 +03:00
Alexey Berezhok
adef1a98bb Fixes 2026-05-10 23:48:56 +03:00
Alexey Berezhok
3263dbecab Fixes 7 2026-05-03 23:58:54 +03:00
Alexey Berezhok
3c5a5924d6 Fixes 5 2026-05-03 19:47:42 +03:00
Alexey Berezhok
8b911372af Fixes 4 2026-05-03 19:47:10 +03:00
Alexey Berezhok
a3888d10b4 Fixes 3 2026-05-03 16:32:53 +03:00
Alexey Berezhok
8add078e7c Added update module 2026-05-03 00:54:14 +03:00
Alexey Berezhok
54f549db62 Fixes2 2026-05-02 17:50:56 +03:00
Alexey Berezhok
fd67d9c546 Fixes 2026-05-01 23:34:16 +03:00
Alexey Berezhok
86bf9fccf5 Added fixes ti bunkerweb module 2026-05-01 22:53:57 +03:00
Alexey Berezhok
f64f034b7b Added setup script 2026-05-01 18:11:49 +03:00
Alexey Berezhok
dae9aca295 Add support of bunkerweb part 2 2026-04-28 00:26:58 +03:00
Alexey Berezhok
592b954a9f Added API for bunkerweb 2026-04-27 00:47:57 +03:00
Alexey Berezhok
4b023ea671 Added install fixes 2026-04-12 00:50:31 +03:00
Alexey Berezhok
4cb55905cc Try to support bunkerweb 2026-04-11 00:41:04 +03:00
364 changed files with 7535 additions and 19104 deletions

View File

@@ -63,7 +63,7 @@ All notable changes to this project will be documented in this file.
## [1.8.1] - Service release ## [1.8.1] - Service release
- Fixed Debian 10 not working with IP addresses check - Fixed Debian 10 not working with IP addresses check
- Fixed Exim4 update config via patch was unreliable added few safety checks and add notice if failed. - Fixed Exim update config via patch was unreliable added few safety checks and add notice if failed.
- Fixed hestia-nginx not loading with custom port - Fixed hestia-nginx not loading with custom port
## [1.8.0] - Feature / Major release ## [1.8.0] - Feature / Major release
@@ -630,8 +630,8 @@ A bug in v-update-sys-hestia caused auto update to be not working. Please run: `
## [1.6.2] - Service release ## [1.6.2] - Service release
- Fixed an issue with rate limits in Exim4 and make it more bullet proof (#2703) - Fixed an issue with rate limits in Exim and make it more bullet proof (#2703)
- Fixed an issue with system-filter not properly loaded in Exim4 for Exim 4.94 and Ugrades from 1.5.x - Fixed an issue with system-filter not properly loaded in Exim for Exim 4.94 and Ugrades from 1.5.x
## [1.6.1] - Service release ## [1.6.1] - Service release
@@ -1238,7 +1238,7 @@ After that run apt update && apt upgrade
- Improve the calculated disk size of a new backup estimated by excluding the exclude folders, mail accounts and database in backups (#1616) @Myself5 - Improve the calculated disk size of a new backup estimated by excluding the exclude folders, mail accounts and database in backups (#1616) @Myself5
- Improve v-update-firewall / v-stop-firewall to make it self healing (#1892) @myrevery - Improve v-update-firewall / v-stop-firewall to make it self healing (#1892) @myrevery
- Update phpMyAdmin version to 1.5.1 (See <https://www.phpmyadmin.net/news/2021/6/4/phpmyadmin-511-released/>) - Update phpMyAdmin version to 1.5.1 (See <https://www.phpmyadmin.net/news/2021/6/4/phpmyadmin-511-released/>)
- Fixed a bug after rebuilding mail with Exim4 and suspended domains (#1886) - Fixed a bug after rebuilding mail with Exim and suspended domains (#1886)
- Fixed "Allowed IP addresses for API" field with strange behaviour #1866 - Fixed "Allowed IP addresses for API" field with strange behaviour #1866
- Fixed an issue where the "Saved confirmation" was not set due to a redirect #1879 - Fixed an issue where the "Saved confirmation" was not set due to a redirect #1879
- Increased minimal memory requirements for ClamD / ClamAV. #1840 - Increased minimal memory requirements for ClamD / ClamAV. #1840

View File

@@ -79,9 +79,9 @@ EOF
chown root:$user $fastcgi chown root:$user $fastcgi
chmod 640 $fastcgi chmod 640 $fastcgi
str="fastcgi_cache_path /var/cache/nginx/micro/$domain levels=1:2" str="fastcgi_cache_path /usr/local/hestia/nginx-system/var/cache/nginx/micro/$domain levels=1:2"
str="$str keys_zone=$domain:10m max_size=512m inactive=30m use_temp_path=off;" str="$str keys_zone=$domain:10m max_size=512m inactive=30m use_temp_path=off;"
conf='/etc/nginx/conf.d/fastcgi_cache_pool.conf' conf='/usr/local/hestia/nginx-system/etc/nginx/conf.d/fastcgi_cache_pool.conf'
if [ -f "$conf" ]; then if [ -f "$conf" ]; then
if [ -z "$(grep "=${domain}:" $conf)" ]; then if [ -z "$(grep "=${domain}:" $conf)" ]; then
echo "$str" >> $conf echo "$str" >> $conf
@@ -90,7 +90,7 @@ else
echo "$str" >> $conf echo "$str" >> $conf
fi fi
mkdir -p /var/cache/nginx/micro/$domain mkdir -p /usr/local/hestia/nginx-system/var/cache/nginx/micro/$domain
#----------------------------------------------------------# #----------------------------------------------------------#
# Hestia # # Hestia #

View File

@@ -122,7 +122,7 @@ if [ -n "$user_rate_limit" ]; then
sed -i "/^$account@$domain_idn:/ d" $HOMEDIR/$user/conf/mail/$domain/limits sed -i "/^$account@$domain_idn:/ d" $HOMEDIR/$user/conf/mail/$domain/limits
echo "$account@$domain_idn:$user_rate_limit" >> $HOMEDIR/$user/conf/mail/$domain/limits echo "$account@$domain_idn:$user_rate_limit" >> $HOMEDIR/$user/conf/mail/$domain/limits
else else
system=$(cat /etc/exim4/limit.conf) system=$(cat /etc/exim/limit.conf)
sed -i "/^$account@$domain_idn:/ d" $HOMEDIR/$user/conf/mail/$domain/limits sed -i "/^$account@$domain_idn:/ d" $HOMEDIR/$user/conf/mail/$domain/limits
echo "$account@$domain_idn:$system" >> $HOMEDIR/$user/conf/mail/$domain/limits echo "$account@$domain_idn:$system" >> $HOMEDIR/$user/conf/mail/$domain/limits
fi fi

View File

@@ -28,11 +28,8 @@ source $HESTIA/func/domain.sh
source_conf "$HESTIA/conf/hestia.conf" source_conf "$HESTIA/conf/hestia.conf"
# Define mail user # Define mail user
if [ "$MAIL_SYSTEM" = 'exim4' ]; then MAIL_USER=exim
MAIL_USER=Debian-exim
else
MAIL_USER=exim
fi
# Additional argument formatting # Additional argument formatting
format_domain format_domain

View File

@@ -27,11 +27,8 @@ source $HESTIA/func/domain.sh
source_conf "$HESTIA/conf/hestia.conf" source_conf "$HESTIA/conf/hestia.conf"
# Define mail user # Define mail user
if [ "$MAIL_SYSTEM" = 'exim4' ]; then MAIL_USER=exim
MAIL_USER=Debian-exim
else
MAIL_USER=exim
fi
# Additional argument formatting # Additional argument formatting
format_domain format_domain

View File

@@ -35,11 +35,7 @@ source $HESTIA/func/syshealth.sh
source_conf "$HESTIA/conf/hestia.conf" source_conf "$HESTIA/conf/hestia.conf"
# Define mail user # Define mail user
if [ "$MAIL_SYSTEM" = 'exim4' ]; then MAIL_USER=exim
MAIL_USER=Debian-exim
else
MAIL_USER=exim
fi
# Additional argument formatting # Additional argument formatting
format_domain format_domain

View File

@@ -27,11 +27,8 @@ source $HESTIA/func/domain.sh
source_conf "$HESTIA/conf/hestia.conf" source_conf "$HESTIA/conf/hestia.conf"
# Define mail user # Define mail user
if [ "$MAIL_SYSTEM" = 'exim4' ]; then MAIL_USER=exim
MAIL_USER=Debian-exim
else
MAIL_USER=exim
fi
# Additional argument formatting # Additional argument formatting
format_domain format_domain

View File

@@ -48,7 +48,7 @@ check_hestia_demo_mode
# Action # # Action #
#----------------------------------------------------------# #----------------------------------------------------------#
cat > /etc/exim4/domains/${domain}/smtp_relay.conf << EOL cat > /etc/exim/domains/${domain}/smtp_relay.conf << EOL
host:$host host:$host
port:$port port:$port
user:$username user:$username

View File

@@ -46,7 +46,7 @@ change_sys_value 'SMTP_RELAY_HOST' "$host"
change_sys_value 'SMTP_RELAY_PORT' "$port" change_sys_value 'SMTP_RELAY_PORT' "$port"
change_sys_value 'SMTP_RELAY_USER' "$username" change_sys_value 'SMTP_RELAY_USER' "$username"
cat > /etc/exim4/smtp_relay.conf << EOL cat > /etc/exim/smtp_relay.conf << EOL
host:$host host:$host
port:$port port:$port
user:$username user:$username

View File

@@ -9,7 +9,7 @@
# www.domain.tld type will be automatically assigned to the domain unless # www.domain.tld type will be automatically assigned to the domain unless
# "none" is transmited as argument. If ip have associated dns name, this # "none" is transmited as argument. If ip have associated dns name, this
# domain will also get the alias domain-tpl.$ipname. An alias with the ip # domain will also get the alias domain-tpl.$ipname. An alias with the ip
# name is useful during the site testing while dns isn't moved to server yet. # name is useful during the site testing while dns isn't moved to server yaliet.
#----------------------------------------------------------# #----------------------------------------------------------#
# Variables & Functions # # Variables & Functions #
@@ -251,6 +251,15 @@ check_result $? "Web restart failed" > /dev/null
$BIN/v-restart-proxy "$restart" $BIN/v-restart-proxy "$restart"
check_result $? "Proxy restart failed" > /dev/null check_result $? "Proxy restart failed" > /dev/null
# Execute bunkerweb_module if it's enabled
module_state=$($BIN/v-ext-modules state bunkerweb_module json | jq -r '.[0].STATE')
if [ "$module_state" = "enabled" ]; then
$BIN/v-ext-modules-run bunkerweb_module add "$domain" "$ip"
if [ -n "%$ALIAS" ]; then
$BIN/v-ext-modules-run bunkerweb_module alias "$domain" "$ALIAS"
fi
fi
# Logging # Logging
$BIN/v-log-action "$user" "Info" "Web" "Added new web domain (Name: $domain)." $BIN/v-log-action "$user" "Info" "Web" "Added new web domain (Name: $domain)."
log_event "$OK" "$ARGUMENTS" log_event "$OK" "$ARGUMENTS"

View File

@@ -108,6 +108,13 @@ check_result $? "Web restart failed" > /dev/null
$BIN/v-restart-proxy "$restart" $BIN/v-restart-proxy "$restart"
check_result $? "Proxy restart failed" > /dev/null check_result $? "Proxy restart failed" > /dev/null
# Execute bunkerweb_module if it's enabled
module_state=$($BIN/v-ext-modules state bunkerweb_module json | jq -r '.[0].STATE')
if [ "$module_state" = "enabled" ]; then
get_domain_values 'web'
$BIN/v-ext-modules-run bunkerweb_module alias "$domain" "$ALIAS"
fi
$BIN/v-log-action "$user" "Info" "Web" "Added new web domain alias (Alias: $aliases, Domain: $domain)." $BIN/v-log-action "$user" "Info" "Web" "Added new web domain alias (Alias: $aliases, Domain: $domain)."
log_event "$OK" "$ARGUMENTS" log_event "$OK" "$ARGUMENTS"

View File

@@ -136,6 +136,25 @@ if [ -n "$UPDATE_SSL_SCRIPT" ]; then
eval "$UPDATE_SSL_SCRIPT $user $domain" eval "$UPDATE_SSL_SCRIPT $user $domain"
fi fi
# Execute bunkerweb_module if it's enabled
module_state=$($BIN/v-ext-modules state bunkerweb_module json | jq -r '.[0].STATE')
if [ "$module_state" = "enabled" ] && [ "$BUNKERWEB" = "yes" ]; then
BUNKW_DIR=$HOMEDIR/$user/conf/web/$domain/ssl/bunkerweb
if [ ! -d "$BUNKW_DIR" ]; then
mkdir -p "$BUNKW_DIR"
chmod 0755 "$BUNKW_DIR"
fi
cp -f "$HOMEDIR/$user/conf/web/$domain/ssl/$domain.crt" "$BUNKW_DIR/"
cp -f "$HOMEDIR/$user/conf/web/$domain/ssl/$domain.key" "$BUNKW_DIR/"
cp -f "$HOMEDIR/$user/conf/web/$domain/ssl/$domain.pem" "$BUNKW_DIR/"
if [ -e "$HOMEDIR/$user/conf/web/$domain/ssl/$domain.ca" ]; then
cp -f "$HOMEDIR/$user/conf/web/$domain/ssl/$domain.ca" "$BUNKW_DIR/"
fi
chown root:nginx "$BUNKW_DIR"/*
chmod 0640 "$BUNKW_DIR"/*
$BIN/v-ext-modules-run bunkerweb_module addssl "$domain" "$BUNKW_DIR/$domain.pem" "$BUNKW_DIR/$domain.key"
fi
# Logging # Logging
$BIN/v-log-action "$user" "Info" "Web" "Added certificate and enabled SSL (Domain: $domain)." $BIN/v-log-action "$user" "Info" "Web" "Added certificate and enabled SSL (Domain: $domain)."
log_event "$OK" "$ARGUMENTS" log_event "$OK" "$ARGUMENTS"

574
bin/v-bunkerweb-migrate Executable file
View File

@@ -0,0 +1,574 @@
#!/opt/brepo/ruby33/bin/ruby
# info: utility to prepare existing server with hestiacp to use bunkerweb
# do not run this script n the server, where bunkerweb was installed with hestiacp
# installation
# options: COMMAND
#
# example: v-bunkerweb-migrate migrate-nginx
#
# Commands:
# migratenginx - move old nginx configs to the new port and path
# migratetobunkerweb - create items of sites in the bunkerweb database
#
#------------------------------------------#
# Variables & Functions #
#------------------------------------------#
# Argument definition
v_command = ARGV[0]
require "/usr/local/hestia/func_ruby/global_options"
load_ruby_options_defaults
$HESTIA = load_hestia_default_path_from_env
require "main"
require "modules"
require "HestiaBunkerWebApi"
require 'json' unless defined?(JSON)
require 'fileutils'
require 'time'
require 'pathname'
def copy_nginx_files(src_root, dest_root)
FileUtils.mkdir_p(dest_root)
Dir.foreach(src_root) do |entry|
next if entry == '.' || entry == '..'
next if entry == 'modules' || entry == 'modules-enabled'
src_path = File.join(src_root, entry)
dest_path = File.join(dest_root, entry)
if File.directory?(src_path)
FileUtils.mkdir_p(dest_path)
copy_nginx_files(src_path, dest_path)
else
FileUtils.cp(src_path, dest_path)
end
end
end
# Function to log and execute migration stages
def log_migrate_stage(stage)
log_file = '/usr/local/hestia/log/bunkerweb_migrate_stages.log'
# Check if the stage has already been recorded
if File.exist?(log_file) && File.readlines(log_file).any? { |line| line.strip == stage }
hestia_print_error_message_to_cli "Stage #{stage} already completed, skipping."
return
end
# Execute the stage block
begin
yield
# Record the successful stage
File.open(log_file, 'a') { |f| f.puts stage }
hestia_print_info_message_to_cli "Stage #{stage} completed."
rescue => e
hestia_print_error_message_to_cli "Stage #{stage} failed: #{e.message}"
exit 1
end
end
# Function to parse and migrate nginx.conf from /usr/local/hestia/nginx-system/etc/nginx/nginx.conf
def migrate_nginx_config_from_file(source_path)
return false unless File.exist?(source_path)
hestia_print_info_message_to_cli "Processing nginx config from: #{source_path}"
content = File.read(source_path)
original_content = content.dup
modified = false
# Replace all paths starting with /var/ to /usr/local/hestia/nginx-system/var/
# This pattern matches any absolute path that starts with /var/ anywhere in the line
content = content.gsub(/\/var\//, '/usr/local/hestia/nginx-system/var/')
# Replace pid path from /run/nginx.pid to /run/nginx-system.pid
content = content.gsub(/pid\s+\S+/) { |match| match.gsub('/run/nginx.pid', '/run/nginx-system.pid') }
if content != original_content
File.write(source_path, content)
hestia_print_info_message_to_cli "Updated config: #{source_path}"
modified = true
end
modified
end
def parse_listen(line)
# Попытка найти IP:port
m = line.match(/^\s*listen\s+([^\s:]+):(\d+)/i)
return [m[1], m[2]] if m
# Если только порт после listen
m = line.match(/^\s*listen\s+(\d+);?\s*$/i)
return [nil, m[1]] if m
nil
end
# Helper function to parse and replace ports in listen directives using temp placeholders
def parse_and_replace_listen_directive(line, proxy_port, proxy_ssl_port)
return line unless line.match?(/\blisten\b/i)
new_line = line.dup
# Define target ports (always migrate to these values regardless of input)
target_http_port = '8078'
target_ssl_port = '8079'
result = parse_listen(line)
return line unless result
ip, port = result
if port == proxy_port
if ip
new_line.gsub!("#{ip}:#{port}", "#{ip}:#{target_http_port}")
else
new_line.gsub!(port, target_http_port)
end
elsif port == proxy_ssl_port
if ip
new_line.gsub!("#{ip}:#{port}", "#{ip}:#{target_ssl_port}")
else
new_line.gsub!(port, target_ssl_port)
end
end
new_line
end
hestia_check_privileged_user
load_global_bash_variables "/etc/hestiacp/hestia.conf"
if $HESTIA.nil?
hestia_print_error_message_to_cli "Can't find HESTIA base path"
exit 1
end
load_global_bash_variables "#{$HESTIA}/conf/hestia.conf"
#------------------------------------------#
# Verifications #
#------------------------------------------#
check_args 1, ARGV, "COMMAND"
# Perform verification if read-only mode is enabled
check_hestia_demo_mode
#------------------------------------------#
# Action #
#------------------------------------------#
case v_command.to_sym
when :migratenginx
log_migrate_stage('stage0') do
# Create backup of /etc/nginx with timestamp
timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
backup_root = "/etc/nginx_backup_#{timestamp}"
FileUtils.mkdir_p(backup_root)
src_root = '/etc/nginx'
dest_root = backup_root
# Custom copy function to handle symlinks in conf.d/domains
def copy_with_symlinks(src, dest)
Dir.foreach(src) do |entry|
next if entry == '.' || entry == '..'
src_path = File.join(src, entry)
dest_path = File.join(dest, entry)
if File.symlink?(src_path)
# Check if symlink is inside conf.d/domains
if src_path.include?(File.join('conf.d', 'domains'))
# Resolve the target of the symlink
target_path = File.readlink(src_path)
# Resolve relative symlink paths
unless Pathname.new(target_path).absolute?
target_path = File.expand_path(target_path, File.dirname(src_path))
end
if File.exist?(target_path) && File.file?(target_path)
content = File.read(target_path)
new_file_name = "#{entry}_content.conf"
new_file_path = File.join(dest, new_file_name)
File.write(new_file_path, content)
end
else
# Preserve the symlink as is
FileUtils.mkdir_p(File.dirname(dest_path))
target = File.readlink(src_path)
FileUtils.ln_s(target, dest_path)
end
elsif File.directory?(src_path)
FileUtils.mkdir_p(dest_path)
copy_with_symlinks(src_path, dest_path)
else
FileUtils.cp(src_path, dest_path)
end
end
end
copy_with_symlinks(src_root, dest_root)
end
log_migrate_stage('stage1') do
if $BUNKERWEB.nil?
hestia_change_sys_config_value("BUNKERWEB", "yes")
end
end
log_migrate_stage('stage2') do
unless system('yum install -y nginx-system')
hestia_print_error_message_to_cli "Failed to install nginx-system via yum"
log_event E_ARGS, $ARGUMENTS
exit 1
end
end
log_migrate_stage('stage3') do
FileUtils.rm_f Dir.glob('/usr/local/hestia/nginx-system/etc/nginx/conf.d/*.conf')
src_root = '/etc/nginx'
dest_root = '/usr/local/hestia/nginx-system/etc/nginx'
copy_nginx_files(src_root, dest_root)
end
log_migrate_stage('stage4') do
# Find all files under the nginx-system directory
nginx_conf_dir = '/usr/local/hestia/nginx-system/etc/nginx'
Dir.glob(File.join(nginx_conf_dir, '**', '*')).each do |path|
hestia_print_info_message_to_cli "stage 4 processing file #{path}"
next if File.directory?(path)
content = File.read(path)
new_content = content.gsub(/(?<!\/usr\/local\/hestia\/nginx-system)\/etc\/nginx/, '/usr/local/hestia/nginx-system/etc/nginx')
if new_content != content
hestia_print_info_message_to_cli "Changed path to config in file #{path}"
File.open(path, 'w') { |f| f.write(new_content) }
end
end
end
log_migrate_stage('stage4.1') do
# Parse nginx.conf file to replace paths
if migrate_nginx_config_from_file("/usr/local/hestia/nginx-system/etc/nginx/nginx.conf")
hestia_print_info_message_to_cli "Completed migration of nginx.conf paths"
else
hestia_print_error_message_to_cli "Warning: Could not migrate nginx.conf from #{File.expand_path('/usr/local/hestia/nginx-system/etc/nginx/nginx.conf')}"
end
end
log_migrate_stage('stage5') do
nginx_conf_dir = '/usr/local/hestia/nginx-system/etc/nginx'
# Read proxy ports from configuration
#proxy_port = $PROXY_PORT.nil? || $PROXY_PORT.empty? ? '80' : $PROXY_PORT
#proxy_ssl_port = $PROXY_SSL_PORT.nil? || $PROXY_SSL_PORT.empty? ? '443' : $PROXY_SSL_PORT
proxy_port = '80'
proxy_ssl_port = '443'
hestia_print_info_message_to_cli "Migrating ports: #{proxy_port} -> 8078, #{proxy_ssl_port} -> 8079"
# Find and replace port in all .conf files
Dir.glob(File.join(nginx_conf_dir, '**', '*.conf')).each do |conf_file|
hestia_print_info_message_to_cli "stage 5 processing file #{conf_file}"
content = File.read(conf_file)
modified = false
# Process line by line - only replace ports in listen directives
new_lines = []
content.each_line do |line|
# Check if line is a listen directive (starts with optional whitespace then 'listen')
if /^\s*listen\s+/i.match?(line) || /^listen\s+/i.match?(line)
# This is a listen line - process it
new_line = parse_and_replace_listen_directive(line, proxy_port, proxy_ssl_port)
modified = true unless new_line == line
new_lines << new_line
else
# Not a listen line, keep as is
new_lines << line
end
end
# Write changes back if modified
if modified
File.open(conf_file, 'w') { |f| f.write(new_lines.join) }
hestia_print_info_message_to_cli " Updated: #{conf_file}"
end
end
end
log_migrate_stage('stage6') do
hestia_change_sys_config_value("PROXY_PORT", "8078")
hestia_change_sys_config_value("PROXY_SSL_PORT", "8079")
end
log_migrate_stage('stage7') do
if system('/usr/local/hestia/bin/v-ext-modules enable update_module')
output = IO.popen("/usr/local/hestia/bin/v-ext-modules state update_module json").read
begin
parsed = JSON.parse(output)
if parsed.is_a?(Array) && parsed.first && parsed.first['STATE'] == 'enabled'
system('/usr/local/hestia/bin/v-ext-modules-run update_module synctemplates')
else
hestia_print_error_message_to_cli "update_module not enabled after enable command"
exit 1
end
rescue JSON::ParserError => e
hestia_print_error_message_to_cli "Failed to parse JSON from state command: #{e.message}"
exit 1
end
else
hestia_print_error_message_to_cli "Failed to enable update_module"
exit 1
end
end
log_migrate_stage('stage8') do
hestia_change_sys_config_value("PROXY_PORT", "8078")
hestia_change_sys_config_value("PROXY_SSL_PORT", "8079")
end
#stage8 активация из запуск nginx-system
log_migrate_stage('stage9') do
# Delete all contents inside /etc/nginx
FileUtils.rm_rf Dir.glob('/etc/nginx/*')
# Stop nginx service
system('systemctl stop nginx')
# Start nginx-system service
system('systemctl enable nginx-system')
system('systemctl start nginx-system')
end
when :migratetobunkerweb
log_migrate_stage('stage10') do
if system('/usr/local/hestia/bin/v-ext-modules enable bunkerweb_module')
output = IO.popen("/usr/local/hestia/bin/v-ext-modules state bunkerweb_module json").read
begin
parsed = JSON.parse(output)
if parsed.is_a?(Array) && parsed.first && parsed.first['STATE'] == 'enabled'
result = system('/usr/local/hestia/bin/v-ext-modules-run bunkerweb_module configure')
unless result
hestia_print_error_message_to_cli "bunkerweb_module configure command failed"
exit 1
end
else
hestia_print_error_message_to_cli "bunkerweb_module not enabled after enable command"
exit 1
end
rescue JSON::ParserError => e
hestia_print_error_message_to_cli "Failed to parse JSON from state command: #{e.message}"
exit 1
end
else
hestia_print_error_message_to_cli "Failed to enable bunkerweb_module"
exit 1
end
hestia_print_info_message_to_cli "Ожидаем минуту для перезапуска сервиса bunkerweb..."
sleep 60
end
LIST_DOMAINS=[]
log_migrate_stage('stage11') do
hestia_print_info_message_to_cli "Stage 11: Migrating users and domains to BunkerWeb..."
# Get all users from HestiaCP in JSON format
user_list_output = IO.popen("/usr/local/hestia/bin/v-list-users json").read
begin
user_data = JSON.parse(user_list_output)
rescue JSON::ParserError => e
hestia_print_error_message_to_cli "Failed to parse users JSON: #{e.message}"
exit 1
end
# Iterate over each user
user_data.each do |username, user_info|
next unless user_info.is_a?(Hash)
web_domains_count = user_info['U_WEB_DOMAINS']
next unless web_domains_count && web_domains_count.to_i > 0
hestia_print_info_message_to_cli "Processing user: #{username} (#{web_domains_count} domains)"
# Get web domains for this user in JSON format
domain_list_output = IO.popen("/usr/local/hestia/bin/v-list-web-domains #{username} json").read
begin
domain_data = JSON.parse(domain_list_output)
rescue JSON::ParserError => e
hestia_print_error_message_to_cli "Failed to parse domains JSON for user #{username}: #{e.message}"
next
end
# Process each domain
domain_data.each do |domain_name, domain_info|
next unless domain_info.is_a?(Hash)
ssl_status = domain_info['SSL'] || 'no'
is_ssl = ssl_status == 'yes'
# Get IP from domain info - should be available in the parsed JSON
proxy_host = domain_info['IP'] || (domain_info['IP6'].present? ? domain_info['IP6'].strip : "127.0.0.1")
proxy_host = proxy_host.nil? || proxy_host.empty? ? "127.0.0.1" : proxy_host
# Create alias list from domain info (ALIAS field contains comma-separated aliases)
raw_aliases = domain_info['ALIAS'] || ''
if raw_aliases && !raw_aliases.empty?
# Hestia uses comma-separated, pass as-is to v-bunkerweb-module (it handles conversion internally)
aliases_list = raw_aliases
else
aliases_list = domain_name # No aliases, use domain name only
end
hestia_print_info_message_to_cli "Processing domain: #{domain_name} (SSL: #{is_ssl}, IP: #{proxy_host})"
begin
# Add domain to BunkerWeb via module script (without SSL)
cmd = "/usr/local/hestia/bin/v-bunkerweb-module add #{domain_name} #{proxy_host}"
puts_cmd = " Added #{domain_name}: Executing command: #{cmd}"
hestia_print_info_message_to_cli puts_cmd
result = system(cmd)
if result
# Команда успешно выполнена
hestia_print_info_message_to_cli " Status: Success"
else
hestia_print_error_message_to_cli " Failed to add domain #{domain_name}"
next
end
rescue => e
hestia_print_error_message_to_cli "Failed to add domain #{domain_name} to BunkerWeb: #{e.message}"
next
end
# Add aliases to the domain via module script
begin
cmd = "/usr/local/hestia/bin/v-bunkerweb-module alias #{domain_name} \"#{aliases_list}\""
puts_cmd = " Added aliases for #{domain_name}: Executing command: #{cmd}"
hestia_print_info_message_to_cli puts_cmd
result = system(cmd)
if result
hestia_print_info_message_to_cli " Status: Success"
else
hestia_print_error_message_to_cli " Failed to set aliases for #{domain_name}"
next
end
rescue => e
hestia_print_error_message_to_cli "Failed to set aliases for #{domain_name}: #{e.message}"
next
end
# Handle SSL configuration if enabled
if is_ssl
original_ssl_dir = "/home/#{username}/conf/web/#{domain_name}/ssl"
# Check if SSL directory and files exist
unless Dir.exist?(original_ssl_dir) || File.exist?("#{original_ssl_dir}/#{domain_name}.pem")
hestia_print_error_message_to_cli "Warning: SSL files not found for #{domain_name}"
LIST_DOMAINS << {
user: username,
domain: domain_name,
is_ssl: true,
path_to_ssl: nil
}
next
end
# Define bunkerweb directory for this domain's certificates
bunkerweb_ssl_dir = "/home/#{username}/conf/web/#{domain_name}/ssl/bunkerweb"
begin
# Create bunkerweb SSL directory if it doesn't exist
FileUtils.mkdir_p(bunkerweb_ssl_dir)
FileUtils.chmod(0755, bunkerweb_ssl_dir)
# Define paths in bunkerweb directory
crt_path_bunkerweb = "#{bunkerweb_ssl_dir}/#{domain_name}.crt"
cert_path_bunkerweb = "#{bunkerweb_ssl_dir}/#{domain_name}.pem"
key_path_bunkerweb = "#{bunkerweb_ssl_dir}/#{domain_name}.key"
# Copy all SSL files if they exist (matching v-add-web-domain-ssl behavior)
original_crt = "#{original_ssl_dir}/#{domain_name}.crt"
original_pem = "#{original_ssl_dir}/#{domain_name}.pem"
original_key = "#{original_ssl_dir}/#{domain_name}.key"
original_ca = "#{original_ssl_dir}/#{domain_name}.ca"
# Copy .crt file if exists
if File.exist?(original_crt)
FileUtils.cp(original_crt, crt_path_bunkerweb)
end
if File.exist?(original_pem)
# Only use .pem if .crt doesn't exist (fallback like the backup script)
FileUtils.cp(original_pem, cert_path_bunkerweb)
end
# Copy .key file if exists
if File.exist?(original_key)
FileUtils.cp(original_key, key_path_bunkerweb)
end
# Copy .ca file if exists
if File.exist?(original_ca)
FileUtils.cp(original_ca, "#{bunkerweb_ssl_dir}/#{domain_name}.ca")
end
# Set ownership and permissions (nginx user can read, others cannot)
FileUtils.chown('root', 'nginx', bunkerweb_ssl_dir)
FileUtils.chmod(0755, bunkerweb_ssl_dir)
FileUtils.chown('root', 'nginx', cert_path_bunkerweb) if File.exist?(cert_path_bunkerweb)
FileUtils.chmod(0640, cert_path_bunkerweb) if File.exist?(cert_path_bunkerweb)
FileUtils.chown('root', 'nginx', key_path_bunkerweb) if File.exist?(key_path_bunkerweb)
FileUtils.chmod(0640, key_path_bunkerweb) if File.exist?(key_path_bunkerweb)
FileUtils.chown('root', 'nginx', crt_path_bunkerweb) if File.exist?(crt_path_bunkerweb)
FileUtils.chmod(0640, crt_path_bunkerweb) if File.exist?(crt_path_bunkerweb)
# Add SSL configuration to BunkerWeb via module script (use proxy_host from earlier)
cmd = "/usr/local/hestia/bin/v-bunkerweb-module addssl #{domain_name} #{crt_path_bunkerweb} #{key_path_bunkerweb}"
puts_cmd = " Added SSL for #{domain_name}: Executing command: #{cmd}"
hestia_print_info_message_to_cli puts_cmd
result = system(cmd)
if result
hestia_print_info_message_to_cli " Status: Success"
else
hestia_print_error_message_to_cli " Failed to configure SSL for #{domain_name}"
end
# Update path_to_ssl to point to bunkerweb directory
ssl_cert_path = crt_path_bunkerweb
rescue => e
hestia_print_error_message_to_cli "Failed to configure SSL for #{domain_name}: #{e.message}"
end
end
# Populate LIST_DOMAINS array with domain info
LIST_DOMAINS << {
user: username,
domain: domain_name,
proxy_host: proxy_host.to_s,
is_ssl: is_ssl == true ? "yes" : "no", # Convert boolean/string to proper string format
path_to_ssl: ssl_cert_path ? ssl_cert_path : nil
}
hestia_print_info_message_to_cli "Successfully migrated #{domain_name} for user #{username}"
end
end
# Output the populated LIST_DOMAINS array
if !LIST_DOMAINS.empty?
hestia_print_info_message_to_cli "\n=== Populated LIST_DOMAINS ==="
LIST_DOMAINS.each_with_index do |entry, idx|
hestia_print_info_message_to_cli "#{idx + 1}. user: #{entry[:user]}, domain: #{entry[:domain]}, is_ssl: #{entry[:is_ssl].to_s}, path_to_ssl: #{entry[:path_to_ssl]}"
end
else
hestia_print_error_message_to_cli "No domains were migrated to BunkerWeb"
end
hestia_print_info_message_to_cli "Stage 11 completed."
end
else
hestia_print_error_message_to_cli "unknown command (use migratetobunkerweb or migratenginx)"
log_event E_ARGS, $ARGUMENTS
exit 1
end
exit 0

267
bin/v-bunkerweb-module Executable file
View File

@@ -0,0 +1,267 @@
#!/opt/brepo/ruby33/bin/ruby
# info: action with bunkerweb API
# options: COMMAND [SERVICE_NAME | SSL_CERT | SSL_KEY | FORMAT]
#
# example: v-bunkerweb-module list json
#
# This function enables and disables additional modules
#
# Commands:
# add [domain_name] [ip]
# addssl [domain_name] [SSL_CERT_PATH] [SSL_KEY_PATH]
# delete [domain_name]
# updssl [domain] [SSL_CERT_PATH] [SSL_KEY_PATH]
# list
#----------------------------------------------------------#
# Variables & Functions #
#----------------------------------------------------------#
# Argument definition
v_command = ARGV[0]
v_format = nil
require "/usr/local/hestia/func_ruby/global_options"
load_ruby_options_defaults
$HESTIA = load_hestia_default_path_from_env
require "main"
require "modules"
require "HestiaBunkerWebApi"
require 'json' unless defined?(JSON)
hestia_check_privileged_user
load_global_bash_variables "/etc/hestiacp/hestia.conf"
if $HESTIA.nil?
hestia_print_error_message_to_cli "Can't find HESTIA base path"
exit 1
end
load_global_bash_variables "#{$HESTIA}/conf/hestia.conf"
#----------------------------------------------------------#
# Verifications #
#----------------------------------------------------------#
check_args 1, ARGV, "COMMAND [COMMAND_OPTIONS] [ACTION]"
# Perform verification if read-only mode is enabled
check_hestia_demo_mode
#----------------------------------------------------------#
# Action #
#----------------------------------------------------------#
case v_command.to_sym
when :add
v_domain = ARGV[1].strip
v_ip = ARGV[2] ? ARGV[2].strip : nil
v_format = ARGV[3] unless ARGV[2].nil?
if v_domain.nil? || v_domain == "" || v_ip.nil? || v_ip == ""
hestia_print_error_message_to_cli "domain and ip should not be empty"
log_event E_ARGS, $ARGUMENTS
exit 1
else
begin
api = HestiaBunkerWebApi.new("http://127.0.0.1:8888")
existing_services = api.list_services()
if existing_services.nil?
result_arr = []
else
if existing_services["services"]
if existing_services["services"].any? { |s| s["id"] == v_domain }
hestia_print_error_message_to_cli "domain already exists"
log_event E_EXISTS, $ARGUMENTS
exit 1
end
result_arr = existing_services["services"]
else
result_arr = []
end
end
api.create_service(v_domain, {
ssl: "no",
reverse_proxy_host: "http://#{v_ip}:#{$PROXY_PORT}"
})
rescue BunkerWebApiError => e
hestia_print_error_message_to_cli "[ERROR] Ошибка API: #{e.message}"
log_event E_INVALID, $ARGUMENTS
exit 1
end
end
when :alias
v_domain = ARGV[1].strip
v_alias = ARGV[2].strip
v_format = ARGV[3] unless ARGV[3].nil?
if v_domain.nil? || v_domain == ""
hestia_print_error_message_to_cli "domain should not be empty"
log_event E_ARGS, $ARGUMENTS
exit 1
else
begin
api = HestiaBunkerWebApi.new("http://127.0.0.1:8888")
existing_services = api.list_services()
if existing_services.nil?
result_arr = []
else
if existing_services["services"]
result_arr = existing_services["services"]
else
result_arr = []
end
end
unless result_arr.any? { |s| s["id"] == v_domain }
hestia_print_error_message_to_cli "domain does not exist"
log_event E_NOTEXIST, $ARGUMENTS
exit 1
end
api.set_alias(v_domain, v_alias)
rescue BunkerWebApiError => e
hestia_print_error_message_to_cli "[ERROR] Ошибка API: #{e.message}"
log_event E_INVALID, $ARGUMENTS
exit 1
end
end
when :delete
v_domain = ARGV[1].strip
v_format = ARGV[2] unless ARGV[2].nil?
if v_domain.nil? || v_domain == ""
hestia_print_error_message_to_cli "domain should not be empty"
log_event E_ARGS, $ARGUMENTS
exit 1
else
begin
api = HestiaBunkerWebApi.new("http://127.0.0.1:8888")
existing_services = api.list_services()
if existing_services.nil?
result_arr = []
else
if existing_services["services"]
result_arr = existing_services["services"]
else
result_arr = []
end
end
unless result_arr.any? { |s| s["id"] == v_domain }
hestia_print_error_message_to_cli "domain does not exist"
log_event E_NOTEXIST, $ARGUMENTS
exit 1
end
api.delete_service(v_domain)
rescue BunkerWebApiError => e
hestia_print_error_message_to_cli "[ERROR] Ошибка API: #{e.message}"
log_event E_INVALID, $ARGUMENTS
exit 1
end
end
when :addssl, :updssl
v_domain = ARGV[1].strip
v_ssl_cert = ARGV[2]
v_ssl_key = ARGV[3]
v_format = ARGV[4] unless ARGV[4].nil?
if v_domain.nil? || v_domain == "" || v_ssl_cert.nil? || v_ssl_key.nil? || !File.exist?(v_ssl_cert) || !File.exist?(v_ssl_key)
hestia_print_error_message_to_cli "domain, SSL cert and key must be provided and must exist"
log_event E_ARGS, $ARGUMENTS
exit 1
else
begin
api = HestiaBunkerWebApi.new("http://127.0.0.1:8888")
existing_services = api.list_services()
if existing_services.nil?
result_arr = []
else
if existing_services["services"]
result_arr = existing_services["services"]
else
result_arr = []
end
end
unless result_arr.any? { |s| s["id"] == v_domain }
hestia_print_error_message_to_cli "domain does not exist"
log_event E_NOTEXIST, $ARGUMENTS
exit 1
end
api.update_service_ssl(v_domain, v_ssl_cert, v_ssl_key)
rescue BunkerWebApiError => e
hestia_print_error_message_to_cli "[ERROR] Ошибка API: #{e.message}"
log_event E_INVALID, $ARGUMENTS
exit 1
end
end
when :deletessl
v_domain = ARGV[1].strip
v_format = ARGV[2] unless ARGV[2].nil?
if v_domain.nil? || v_domain == ""
hestia_print_error_message_to_cli "domain should not be empty"
log_event E_ARGS, $ARGUMENTS
exit 1
else
begin
api = HestiaBunkerWebApi.new("http://127.0.0.1:8888")
existing_services = api.list_services()
if existing_services.nil?
result_arr = []
else
if existing_services["services"]
result_arr = existing_services["services"]
else
result_arr = []
end
end
unless result_arr.any? { |s| s["id"] == v_domain }
hestia_print_error_message_to_cli "domain does not exist"
log_event E_NOTEXIST, $ARGUMENTS
exit 1
end
api.delete_service_ssl(v_domain)
rescue BunkerWebApiError => e
hestia_print_error_message_to_cli "[ERROR] Ошибка API: #{e.message}"
log_event E_INVALID, $ARGUMENTS
exit 1
end
end
when :list
v_format = ARGV[1] unless ARGV[1].nil?
begin
api = HestiaBunkerWebApi.new("http://127.0.0.1:8888")
existing_services = api.list_services()
if existing_services.nil?
result_arr = []
else
if existing_services["services"]
result_arr = existing_services["services"]
else
result_arr = []
end
end
hestia_print_array_of_hashes(result_arr, v_format, "id, method, is_draft, creation_date, last_update, template, security_mode")
rescue BunkerWebApiError => e
hestia_print_error_message_to_cli "[ERROR] Ошибка API: #{e.message}"
log_event E_INVALID, $ARGUMENTS
exit 1
end
else
hestia_print_error_message_to_cli "unknown command"
log_event E_ARGS, $ARGUMENTS
exit 1
end
exit 0

488
bin/v-bunkerweb-module-install Executable file
View File

@@ -0,0 +1,488 @@
#!/opt/brepo/ruby33/bin/ruby
# info: action with bunkerweb API
# options: [SSL_CERT_PATH SSL_KEY_PATH]
#
# example: v-bunkerweb-module-install
#
# This function enables and disables additional modules
#
#------------------------------------------#
# Variables & Functions #
#------------------------------------------#
# Argument definition
require "/usr/local/hestia/func_ruby/global_options"
load_ruby_options_defaults
$HESTIA = load_hestia_default_path_from_env
require "main"
require "modules"
require "HestiaBunkerWebApi"
require "securerandom"
require "socket"
require 'json' unless defined?(JSON)
require 'etc'
hestia_check_privileged_user
load_global_bash_variables "/etc/hestiacp/hestia.conf"
if $HESTIA.nil?
hestia_print_error_message_to_cli "Can't find HESTIA base path"
exit 1
end
load_global_bash_variables "#{$HESTIA}/conf/hestia.conf"
# Perform verification if read-only mode is enabled
check_hestia_demo_mode
#------------------------------------------#
# Action #
#------------------------------------------#
puts "========================================================"
puts "=== AUTOMATED BUNKERWEB SETUP SCRIPT ==="
puts "========================================================"
puts ""
# Parse command line arguments for SSL cert and key paths
SSL_CERT_PATH = ARGV[0] || nil # First argument: SSL certificate path
SSL_KEY_PATH = ARGV[1] || nil # Second argument: SSL key path
# Generate secure passwords (meeting BunkerWeb password policy requirements)
api_env_file = "/etc/bunkerweb/api.env"
api_password = nil
if File.exist?(api_env_file)
File.foreach(api_env_file) do |line|
if line =~ /^\s*API_PASSWORD=(.*)/
val = $1.strip
api_password = val unless val.empty?
break
end
end
end
specials = '!@#$%^&*()-_=+[]{}|;:,.<>?'
API_PASSWORD = api_password || (SecureRandom.alphanumeric(24) + specials.chars.sample(3).join).chars.shuffle.join
ui_env_file = "/etc/bunkerweb/ui.env"
admin_password = nil
if File.exist?(ui_env_file)
File.foreach(ui_env_file) do |line|
if line =~ /^\s*ADMIN_PASSWORD=(.*)/
val = $1.strip
admin_password = val unless val.empty?
break
end
end
end
ADMIN_PASSWORD = admin_password || (SecureRandom.alphanumeric(24) + specials.chars.sample(3).join).chars.shuffle.join
puts "[INFO] Get server IP address"
server_ip_addr = "127.0.0.1" # default fallback
# Attempt to retrieve server IP via Hestia utility
begin
cmd = "/usr/local/hestia/bin/v-list-sys-ips json"
ips_output = `#{cmd}`.strip
unless ips_output.empty?
parsed_ips = JSON.parse(ips_output)
parsed_ips.each do |ip, details|
if details["OWNER"] == "admin"
server_ip_addr = ip
break
end
end
end
rescue JSON::ParserError, Errno::ENOENT
# If the command fails or output is invalid, keep default fallback
end
puts ""
# Compute nginx group ID once
nginx_gid = Etc.getgrnam('nginx').gid
# Step 1: Create /etc/bunkerweb/api.env configuration file
puts "[INFO] Creating API configuration at /etc/bunkerweb/api.env..."
api_env_content = <<~APIENV
# ==============================
# BunkerWeb API Configuration
# This file lists all supported API environment variables with their defaults.
# Uncomment and adjust as needed. Lines starting with # are ignored.
# ==============================
# --- Network & Proxy ---
# Listen address/port for the API
LISTEN_ADDR=127.0.0.1
LISTEN_PORT=8888
# Trusted proxy IPs for X-Forwarded-* headers (comma-separated).
# Default is restricted to loopback for security.
FORWARDED_ALLOW_IPS=127.0.0.1,::1
# Trusted proxy IPs for PROXY protocol (comma-separated).
# Defaults to FORWARDED_ALLOW_IPS when unset.
PROXY_ALLOW_IPS=127.0.0.1,::1
# --- Logging & Runtime ---
# LOG_LEVEL affects most components; CUSTOM_LOG_LEVEL overrides when provided.
# LOG_LEVEL=info
LOG_TYPES=file
LOG_FILE_PATH=/var/log/bunkerweb/api.log
# Number of workers/threads (auto if unset).
# MAX_WORKERS=<auto>
# MAX_THREADS=<auto>
# --- Authentication & Authorization ---
# Optional admin Bearer token (grants full access when provided).
# API_TOKEN=#{API_PASSWORD}
# Bootstrap admin user (created/validated on startup if provided).
API_USERNAME=admin
API_PASSWORD=#{API_PASSWORD}
# Force re-applying bootstrap admin credentials on startup (use with care).
# OVERRIDE_API_CREDS=no
# Fine-grained ACLs can be enabled/disabled here.
# API_ACL_BOOTSTRAP_FILE=
# --- IP allowlist ---
# Enable and shape inbound IP allowlist for the API.
API_WHITELIST_ENABLED=yes
WHITELIST_IPS=127.0.0.1
# --- FastAPI surface ---
# Customize or disable documentation endpoints. Use 'disabled' to turn off.
# API_TITLE=BunkerWeb API
# API_DOCS_URL=/docs
# API_REDOC_URL=/redoc
# API_OPENAPI_URL=/openapi.json
# Mount the API under a subpath (useful behind reverse proxies).
# API_ROOT_PATH=
# --- TLS/SSL ---
# Enable TLS for the API listener (requires cert and key).
# API_SSL_ENABLED=no
# Path to PEM-encoded certificate and private key.
# API_SSL_CERTFILE=/etc/ssl/certs/bunkerweb-api.crt
# API_SSL_KEYFILE=/etc/ssl/private/bunkerweb-api.key
# Optional chain/CA bundle and cipher suite.
# API_SSL_CA_CERTS=
# API_SSL_CIPHERS_CUSTOM=
# API_SSL_CIPHERS_LEVEL=modern # choices: modern|intermediate
# --- Biscuit keys & policy ---
# Bind token to client IP (except private ranges).
# CHECK_PRIVATE_IP=yes
# Biscuit token lifetime in seconds (0 disables expiry).
# API_BISCUIT_TTL_SECONDS=3600
# Provide Biscuit keys via env (hex) instead of files.
# BISCUIT_PUBLIC_KEY=
# BISCUIT_PRIVATE_KEY=
# --- Rate limiting ---
# Enable/disable and shape rate limiting.
API_RATE_LIMIT_ENABLED=no
API_RATE_LIMIT_HEADERS_ENABLED=no
# Global default limit (times per seconds).
# API_RATE_LIMIT_TIMES=100
# API_RATE_LIMIT_SECONDS=60
# Authentication endpoint limit.
# API_RATE_LIMIT_AUTH_TIMES=10
# API_RATE_LIMIT_AUTH_SECONDS=60
# Advanced limits and rules (CSV/JSON/YAML).
# API_RATE_LIMIT_DEFAULTS="200/minute"
# API_RATE_LIMIT_APPLICATION_LIMITS=
# API_RATE_LIMIT_RULES=
# Strategy: fixed-window | moving-window | sliding-window-counter
# API_RATE_LIMIT_STRATEGY=fixed-window
# Key selector: ip | user | path | method | header:<Name>
# API_RATE_LIMIT_KEY=ip
# Exempt IPs (space or comma-separated CIDRs).
# API_RATE_LIMIT_EXEMPT_IPS=
# Storage options in JSON (merged with Redis settings if USE_REDIS=yes).
# API_RATE_LIMIT_STORAGE_OPTIONS=
# --- Redis (optional, for rate limiting storage) ---
# USE_REDIS=no
# REDIS_HOST=
# REDIS_PORT=6379
# REDIS_DATABASE=0
# REDIS_USERNAME=
# REDIS_PASSWORD=
# REDIS_SSL=no
# REDIS_SSL_VERIFY=yes
# REDIS_TIMEOUT=1000
# REDIS_KEEPALIVE_POOL=10
# REDIS_SENTINEL_HOSTS=sentinel1:26379 sentinel2:26379
# REDIS_SENTINEL_MASTER=mymaster
# REDIS_SENTINEL_USERNAME=
# REDIS_SENTINEL_PASSWORD=
APIENV
File.write("/etc/bunkerweb/api.env", api_env_content)
File.chmod(0o660, "/etc/bunkerweb/api.env")
File.chown(0, nginx_gid, "/etc/bunkerweb/api.env")
puts "[SUCCESS] API configuration file created at /etc/bunkerweb/api.env"
puts ""
variables_env_content = <<~VENV
DNS_RESOLVERS=9.9.9.9 149.112.112.112 8.8.8.8 8.8.4.4
HTTP_PORT=80
HTTPS_PORT=443
API_LISTEN_IP=127.0.0.1
MULTISITE=yes
UI_HOST=http://127.0.0.1:7000
SERVER_NAME=
API_WHITELIST_IP=127.0.0.0/8
USE_SERVE_FILES=no
VENV
File.write("/etc/bunkerweb/variables.env", variables_env_content)
File.chmod(0o660, "/etc/bunkerweb/variables.env")
File.chown(0, nginx_gid, "/etc/bunkerweb/variables.env")
puts "[SUCCESS] Variables configuration file created at /etc/bunkerweb/variables.env"
puts ""
# Step 2: Enable and start bunkerweb-api service, wait for it to be running
puts "[INFO] Enabling bunkerweb-api service..."
system("systemctl enable bunkerweb-api")
puts "[INFO] Starting bunkerweb-api service..."
system("systemctl start bunkerweb-api")
sleep(30)
# Wait for the service to be ready (max 30 seconds)
puts "[INFO] Waiting for bunkerweb-api service to be running..."
max_attempts = 60 # Wait up to 30 seconds (check every half second)
attempt = 0
while attempt < max_attempts
status_output = `systemctl is-active bunkerweb-api 2>&1`
status = status_output.strip
if status == "active" || status == "running"
puts "[SUCCESS] bunkerweb-api service is running!"
break
elsif status == "failed"
puts "[ERROR] bunkerweb-api service failed to start!"
exit 1
else
print "."
sleep(0.5)
attempt += 1
end
end
if attempt >= max_attempts
puts ""
puts "[ERROR] bunkerweb-api service did not become active within timeout"
puts "[INFO] Current status: #{status_output.strip}"
log_event E_INVALID, $ARGUMENTS
exit 1
end
puts ""
# Step 3: Configure UI settings based on documentation at https://docs.bunkerweb.io/latest/web-ui/
puts "[INFO] Configuring Web UI..."
# Determine SSL settings for UI
ui_ssl_enabled = "no"
if SSL_CERT_PATH && SSL_KEY_PATH && File.exist?(SSL_CERT_PATH) && File.exist?(SSL_KEY_PATH)
ui_ssl_enabled = "yes"
end
ui_env_content = <<~UIENV
# ==============================
# BunkerWeb UI Configuration
# This file configures the Web UI settings.
# ==============================
# --- Listener & TLS ---
# Bind address for the UI (use server IP for external access)
UI_LISTEN_ADDR=127.0.0.1
# Bind port for the UI
UI_LISTEN_PORT=7000
# Enable TLS in the UI container
UI_SSL_ENABLED=#{ui_ssl_enabled}
UIENV
# Add SSL cert/key paths if provided
if ui_ssl_enabled == "yes"
ui_env_content += <<~SSLCONF
# SSL Certificate and Key paths
UI_SSL_CERTFILE=#{SSL_CERT_PATH}
UI_SSL_KEYFILE=#{SSL_KEY_PATH}
SSLCONF
end
ui_env_content += <<~UIENV2
# --- Admin Authentication ---
# Seed admin account
ADMIN_USERNAME=admin
ADMIN_PASSWORD=#{ADMIN_PASSWORD}
# --- Proxy settings ---
# Trusted proxy IPs for X-Forwarded-* headers
# UI_FORWARDED_ALLOW_IPS=127.0.0.1,::1
UIENV2
File.write("/etc/bunkerweb/ui.env", ui_env_content)
File.chmod(0o660, "/etc/bunkerweb/ui.env")
File.chown(0, nginx_gid, "/etc/bunkerweb/ui.env")
puts "[SUCCESS] UI configuration file created at /etc/bunkerweb/ui.env"
puts ""
# Step 4: Reload the bunkerweb-ui service to apply new configuration
puts "[INFO] Reloading bunkerweb-ui service..."
system("systemctl restart bunkerweb-ui")
# Wait for UI to be ready (max 10 seconds)
sleep(2)
if system("systemctl is-active bunkerweb-ui >/dev/null 2>&1")
puts "[SUCCESS] bunkerweb-ui service is running!"
else
puts "[WARN] bunkerweb-ui service status could not be verified"
end
puts ""
# Step 5: Now proceed with the original service creation logic
puts "[INFO] Connecting to BunkerWeb API..."
API_URL = "http://127.0.0.1:8888"
USERNAME = "admin"
PASSWORD = API_PASSWORD
# Default services to create after setup
DEFAULT_SERVICES = [
{
name: "#{server_ip_addr}",
options: {
ssl: "no",
reverse_proxy_host: "http://127.0.0.1:7000",
use_template: "ui",
reverse_proxy_url: "/kormilo",
use_reverse_proxy: "yes"
}
},
# Add more services here if needed:
# {
# name: "secure.example.com",
# options: {
# ssl: "yes",
# certificate_path: "/etc/ssl/certs/example.crt",
# key_path: "/etc/ssl/private/example.key"
# }
# }
]
begin
api = HestiaBunkerWebApi.new(API_URL, USERNAME, PASSWORD)
puts ""
puts "[SUCCESS] API connected successfully!"
puts ""
# List existing services
services = api.list_services()
services = api.list_services()
if services && services.is_a?(Hash) && services.key?('services')
services = services['services']
services = nil if services.is_a?(Array) && services.empty?
else
services = nil
end
if services.nil?
puts "[INFO] No services found - creating default configuration..."
DEFAULT_SERVICES.each do |service_config|
begin
puts "[INFO] Creating service: #{service_config[:name]}"
result = api.create_service(service_config[:name], service_config[:options])
puts " ✓ Service '#{service_config[:name]}' created"
rescue BunkerWebApiError => e
if e.message.include?("already exists")
puts " Service '#{service_config[:name]}' already exists, skipping..."
else
raise
end
end
end
puts "[SUCCESS] Default services created!"
else
puts "[INFO] Existing services:"
services.each { |s| puts " - #{s['server_name']}" }
puts ""
end
# Reload configuration on all instances
puts "[INFO] Reloading configuration..."
api.reload_instance()
puts "[INFO] Restarting bunkerweb service..."
system("systemctl restart bunkerweb")
puts "[INFO] Restarting bunkerweb-scheduler service..."
system("systemctl restart bunkerweb-scheduler")
puts ""
puts "======================================================"
puts "=== SETUP COMPLETED SUCCESSFULLY ==="
puts "======================================================"
puts ""
puts "Web UI is now accessible at:"
if ui_ssl_enabled == "yes"
puts " https://#{server_ip_addr}/kormilo"
else
puts " http://#{server_ip_addr}/kormilo"
end
puts ""
puts "API URL: #{API_URL}"
puts ""
puts "API Credentials:"
puts " Username: admin"
puts " Password: #{PASSWORD}"
puts ""
puts "UI Credentials:"
puts " Username: admin"
puts " Password: #{ADMIN_PASSWORD}"
puts ""
rescue BunkerWebApiError => e
if e.message.include?("Authentication") || e.message.include?("Connection refused")
puts "[ERROR] Could not connect to BunkerWeb API"
puts "[INFO] This means the setup has NOT been completed correctly"
puts ""
puts "Please verify that:"
puts " 1. bunkerweb-api service is running: systemctl status bunkerweb-api"
puts " 2. API configuration file exists at /etc/bunkerweb/api.env"
puts " 3. Check logs: journalctl -u bunkerweb-api -f"
puts ""
log_event E_INVALID, $ARGUMENTS
exit 1
else
puts "[ERROR] #{e.message}"
log_event E_INVALID, $ARGUMENTS
exit 1
end
rescue => e
puts "[ERROR] Unexpected error: #{e.message}"
puts "Backtrace:"
puts e.backtrace.inspect
log_event E_INVALID, $ARGUMENTS
exit 1
end
exit 0

View File

@@ -229,7 +229,7 @@ if [ -n "$mail_data" ]; then
echo " ssl_key = <$HOMEDIR/$user/conf/mail/$domain/ssl/$domain.key" >> /etc/dovecot/conf.d/domains/$domain.conf echo " ssl_key = <$HOMEDIR/$user/conf/mail/$domain/ssl/$domain.key" >> /etc/dovecot/conf.d/domains/$domain.conf
echo "}" >> /etc/dovecot/conf.d/domains/$domain.conf echo "}" >> /etc/dovecot/conf.d/domains/$domain.conf
# Add domain SSL configuration to exim4 # Add domain SSL configuration to exim
# Cleanup symlinks # Cleanup symlinks
find /usr/local/hestia/ssl/mail -xtype l -delete find /usr/local/hestia/ssl/mail -xtype l -delete

View File

@@ -66,7 +66,7 @@ if [[ "$MAIL_SYSTEM" =~ exim ]]; then
sed -i "/^$account@$domain_idn:/ d" $HOMEDIR/$user/conf/mail/$domain/limits sed -i "/^$account@$domain_idn:/ d" $HOMEDIR/$user/conf/mail/$domain/limits
echo "$account@$domain_idn:$user_rate_limit" >> $HOMEDIR/$user/conf/mail/$domain/limits echo "$account@$domain_idn:$user_rate_limit" >> $HOMEDIR/$user/conf/mail/$domain/limits
else else
system=$(cat /etc/exim4/limit.conf) system=$(cat /etc/exim/limit.conf)
sed -i "/^$account@$domain_idn:/ d" $HOMEDIR/$user/conf/mail/$domain/limits sed -i "/^$account@$domain_idn:/ d" $HOMEDIR/$user/conf/mail/$domain/limits
echo "$account@$domain_idn:$system" >> $HOMEDIR/$user/conf/mail/$domain/limits echo "$account@$domain_idn:$system" >> $HOMEDIR/$user/conf/mail/$domain/limits
fi fi

View File

@@ -68,13 +68,13 @@ if [ "$type" = "pma" ] || [ "$type" = "PMA" ] || [ "$type" = "phpmyadmin" ]; the
$BIN/v-restart-service httpd $BIN/v-restart-service httpd
fi fi
if [ -e "/etc/nginx/conf.d/phpmyadmin.inc" ]; then if [ -e "/usr/local/hestia/nginx-system/etc/nginx/conf.d/phpmyadmin.inc" ]; then
rm -f /etc/nginx/conf.d/phpmyadmin.inc rm -f /usr/local/hestia/nginx-system/etc/nginx/conf.d/phpmyadmin.inc
cp -f $HESTIA_INSTALL_DIR/nginx/phpmyadmin.inc /etc/nginx/conf.d/phpmyadmin.inc cp -f $HESTIA_INSTALL_DIR/nginx/phpmyadmin.inc /usr/local/hestia/nginx-system/etc/nginx/conf.d/phpmyadmin.inc
sed -i "s|%pma_alias%|$alias|g" /etc/nginx/conf.d/phpmyadmin.inc sed -i "s|%pma_alias%|$alias|g" /usr/local/hestia/nginx-system/etc/nginx/conf.d/phpmyadmin.inc
# Restart services # Restart services
$BIN/v-restart-service nginx $BIN/v-restart-service nginx-system
fi fi
fi fi
@@ -105,13 +105,13 @@ if [ "$type" = "pga" ] || [ "$type" = "PGA" ] || [ "$type" = "phppgadmin" ]; the
$BIN/v-restart-service httpd $BIN/v-restart-service httpd
fi fi
if [ -e "/etc/nginx/conf.d/phppgadmin.inc" ]; then if [ -e "/usr/local/hestia/nginx-system/etc/nginx/conf.d/phppgadmin.inc" ]; then
rm -f /etc/nginx/conf.d/phppgadmin.inc rm -f /usr/local/hestia/nginx-system/etc/nginx/conf.d/phppgadmin.inc
cp -f $HESTIA_INSTALL_DIR/nginx/phppgadmin.inc /etc/nginx/conf.d/phppgadmin.inc cp -f $HESTIA_INSTALL_DIR/nginx/phppgadmin.inc /usr/local/hestia/nginx-system/etc/nginx/conf.d/phppgadmin.inc
sed -i "s|%pga_alias%|$alias|g" /etc/nginx/conf.d/phppgadmin.inc sed -i "s|%pga_alias%|$alias|g" /usr/local/hestia/nginx-system/etc/nginx/conf.d/phppgadmin.inc
# Restart services # Restart services
$BIN/v-restart-service nginx $BIN/v-restart-service nginx-system
fi fi
fi fi

View File

@@ -65,18 +65,18 @@ fi
# Defining dst config path # Defining dst config path
case $service in case $service in
nginx) dst='/etc/nginx/nginx.conf' ;; nginx) dst='/usr/local/hestia/nginx-system/etc/nginx/nginx.conf' ;;
httpd) dst='/etc/httpd/conf/httpd.conf' ;; httpd) dst='/etc/httpd/conf/httpd.conf' ;;
apache2) dst='/etc/apache2/apache2.conf' ;; apache2) dst='/etc/apache2/apache2.conf' ;;
exim) dst='/etc/exim/exim.conf' ;; exim) dst='/etc/exim/exim.conf' ;;
exim4) dst='/etc/exim4/exim4.conf.template' ;; exim4) dst='/etc/exim/exim.conf.template' ;;
vsftpd) dst=$(find /etc/vsftpd* -name 'vsftpd.conf') ;; vsftpd) dst=$(find /etc/vsftpd* -name 'vsftpd.conf') ;;
proftpd) dst=$(find /etc/proftpd* -name 'proftpd.conf') ;; proftpd) dst=$(find /etc/proftpd* -name 'proftpd.conf') ;;
php) php)
if [ "$LOCAL_PHP" == "yes" ]; then if [ "$LOCAL_PHP" == "yes" ]; then
dst=$(find /opt/brepo/php${php_v} -name php.ini) dst=$(find /opt/brepo/php${php_v} -name php.ini)
else else
dst=$(find /etc/opt/remi/php${php_v} -name php.ini) dst=$(find /etc/opt/remi/php${php_v} -name php.ini)
fi fi
;; ;;
mysql) dst=$(find /etc/my* -name my.cnf) ;; mysql) dst=$(find /etc/my* -name my.cnf) ;;
@@ -136,7 +136,7 @@ if [ "$update" = 'yes' ] && [ "$restart" != 'no' ]; then
fi fi
fi fi
$BIN/v-restart-service "$service" > /dev/null 2>&1 $BIN/v-restart-service "$service" > /dev/null 2>&1
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
for config in $dst; do for config in $dst; do

View File

@@ -88,6 +88,25 @@ check_result $? "Web restart failed" > /dev/null
$BIN/v-restart-proxy "$restart" $BIN/v-restart-proxy "$restart"
check_result $? "Proxy restart failed" > /dev/null check_result $? "Proxy restart failed" > /dev/null
# Execute bunkerweb_module if it's enabled
module_state=$($BIN/v-ext-modules state bunkerweb_module json | jq -r '.[0].STATE')
if [ "$module_state" = "enabled" ] && [ "$BUNKERWEB" = "yes" ]; then
BUNKW_DIR=$HOMEDIR/$user/conf/web/$domain/ssl/bunkerweb
if [ ! -d "$BUNKW_DIR" ]; then
mkdir -p "$BUNKW_DIR"
chmod 0755 "$BUNKW_DIR"
fi
cp -f "$HOMEDIR/$user/conf/web/$domain/ssl/$domain.crt" "$BUNKW_DIR/"
cp -f "$HOMEDIR/$user/conf/web/$domain/ssl/$domain.key" "$BUNKW_DIR/"
cp -f "$HOMEDIR/$user/conf/web/$domain/ssl/$domain.pem" "$BUNKW_DIR/"
if [ -e "$HOMEDIR/$user/conf/web/$domain/ssl/$domain.ca" ]; then
cp -f "$HOMEDIR/$user/conf/web/$domain/ssl/$domain.ca" "$BUNKW_DIR/"
fi
chown root:nginx "$BUNKW_DIR"/*
chmod 0640 "$BUNKW_DIR"/*
$BIN/v-ext-modules-run bunkerweb_module addssl "$domain" "$BUNKW_DIR/$domain.pem" "$BUNKW_DIR/$domain.key"
fi
# Logging # Logging
$BIN/v-log-action "$user" "Info" "Web" "SSL certificate changed (Domain: $domain)." $BIN/v-log-action "$user" "Info" "Web" "SSL certificate changed (Domain: $domain)."
log_event "$OK" "$ARGUMENTS" log_event "$OK" "$ARGUMENTS"

View File

@@ -31,7 +31,7 @@ SERVICE_NAME="$1"
case "$SERVICE_NAME" in case "$SERVICE_NAME" in
nginx ) nginx )
/usr/sbin/nginx -t >> "$DEBUG_LOG_FILE" 2>&1 /usr/local/hestia/nginx-system/sbin/nginx -t >> "$DEBUG_LOG_FILE" 2>&1
V_RESULT=$? V_RESULT=$?
exit $V_RESULT exit $V_RESULT
;; ;;
@@ -47,5 +47,3 @@ esac
# Something like error, we shouldn't be here # Something like error, we shouldn't be here
exit 1 exit 1

View File

@@ -49,7 +49,7 @@ if [ -f "$HOMEDIR/$user/conf/web/$domain/$WEB_SYSTEM.fastcgi_cache.conf" ]; then
rm -rf $HOMEDIR/$user/conf/web/$domain/$WEB_SYSTEM.fastcgi_cache.conf rm -rf $HOMEDIR/$user/conf/web/$domain/$WEB_SYSTEM.fastcgi_cache.conf
fi fi
conf='/etc/nginx/conf.d/fastcgi_cache_pool.conf' conf='/usr/local/hestia/nginx-system/etc/nginx/conf.d/fastcgi_cache_pool.conf'
if [ -f "$conf" ]; then if [ -f "$conf" ]; then
sed -i "/ keys_zone=$domain/d" $conf sed -i "/ keys_zone=$domain/d" $conf
if [ ! -s "$conf" ]; then if [ ! -s "$conf" ]; then
@@ -58,7 +58,7 @@ if [ -f "$conf" ]; then
fi fi
# Delete FastCGI cache folder # Delete FastCGI cache folder
if [ -d "/var/cache/nginx/micro/$domain" ]; then if [ -d "/usr/local/hestia/nginx-system/var/cache/nginx/micro/$domain" ]; then
rm -rf /var/cache/nginx/micro/$domain rm -rf /var/cache/nginx/micro/$domain
fi fi

View File

@@ -37,7 +37,7 @@ is_object_unsuspended 'mail' 'DOMAIN' "$domain"
# Action # # Action #
#----------------------------------------------------------# #----------------------------------------------------------#
rm -f /etc/exim4/domains/${domain}/smtp_relay.conf rm -f /etc/exim/domains/${domain}/smtp_relay.conf
#----------------------------------------------------------# #----------------------------------------------------------#
# Hestia # # Hestia #

View File

@@ -34,7 +34,7 @@ change_sys_value "SMTP_RELAY_HOST" ""
change_sys_value "SMTP_RELAY_PORT" "" change_sys_value "SMTP_RELAY_PORT" ""
change_sys_value "SMTP_RELAY_USER" "" change_sys_value "SMTP_RELAY_USER" ""
rm -f /etc/exim4/smtp_relay.conf rm -f /etc/exim/smtp_relay.conf
#----------------------------------------------------------# #----------------------------------------------------------#
# Hestia # # Hestia #

View File

@@ -170,6 +170,12 @@ check_result $? "Proxy restart failed" > /dev/null
$BIN/v-restart-web-backend "$restart" "$version" $BIN/v-restart-web-backend "$restart" "$version"
check_result $? "PHP restart failed" > /dev/null check_result $? "PHP restart failed" > /dev/null
# Execute bunkerweb_module if it's enabled
module_state=$($BIN/v-ext-modules state bunkerweb_module json | jq -r '.[0].STATE')
if [ "$module_state" = "enabled" ]; then
$BIN/v-ext-modules-run bunkerweb_module delete "$domain"
fi
# Logging # Logging
$BIN/v-log-action "$user" "Info" "Web" "Deleted web domain (Name: $domain)." $BIN/v-log-action "$user" "Info" "Web" "Deleted web domain (Name: $domain)."
log_event "$OK" "$ARGUMENTS" log_event "$OK" "$ARGUMENTS"

View File

@@ -92,6 +92,13 @@ check_result $? "Web restart failed" > /dev/null
$BIN/v-restart-proxy "$restart" $BIN/v-restart-proxy "$restart"
check_result $? "Proxy restart failed" > /dev/null check_result $? "Proxy restart failed" > /dev/null
# Execute bunkerweb_module if it's enabled
module_state=$($BIN/v-ext-modules state bunkerweb_module json | jq -r '.[0].STATE')
if [ "$module_state" = "enabled" ]; then
get_domain_values 'web'
$BIN/v-ext-modules-run bunkerweb_module alias "$domain" "$ALIAS"
fi
# Logging # Logging
$BIN/v-log-action "$user" "Info" "Web" "Deleted web domain alias (Alias: $dom_alias, Domain: $domain)." $BIN/v-log-action "$user" "Info" "Web" "Deleted web domain alias (Alias: $dom_alias, Domain: $domain)."
log_event "$OK" "$ARGUMENTS" log_event "$OK" "$ARGUMENTS"

View File

@@ -90,6 +90,16 @@ check_result $? "Web restart failed" > /dev/null
$BIN/v-restart-proxy "$restart" $BIN/v-restart-proxy "$restart"
check_result $? "Proxy restart failed" > /dev/null check_result $? "Proxy restart failed" > /dev/null
# Execute bunkerweb_module if it's enabled
module_state=$($BIN/v-ext-modules state bunkerweb_module json | jq -r '.[0].STATE')
if [ "$module_state" = "enabled" ] && [ "$BUNKERWEB" = "yes" ]; then
BUNKW_DIR=$HOMEDIR/$user/conf/web/$domain/ssl/bunkerweb
if [ -d "$BUNKW_DIR" ]; then
$BIN/v-ext-modules-run bunkerweb_module deletessl "$domain"
rm -f $BUNKW_DIR/$domain.*
fi
fi
# Logging # Logging
$BIN/v-log-action "$user" "Warning" "Web" "SSL disabled (Domain: $domain)." $BIN/v-log-action "$user" "Warning" "Web" "SSL disabled (Domain: $domain)."
log_event "$OK" "$ARGUMENTS" log_event "$OK" "$ARGUMENTS"

View File

@@ -39,14 +39,10 @@ echo -en "\n---------------------------------------------"
echo -en "---------------------------------------------\n\n" echo -en "---------------------------------------------\n\n"
# Displaying exim stats # Displaying exim stats
if [ -e "/var/log/exim4/mainlog" ]; then eximstats /var/log/exim/main.log 2> /dev/null
eximstats /var/log/exim4/mainlog 2> /dev/null
else
eximstats /var/log/exim/main.log 2> /dev/null
fi
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "[Exim4] No valid log lines read" echo "[Exim] No valid log lines read"
exit 0 exit 0
fi fi

View File

@@ -65,7 +65,7 @@ csv_list() {
#----------------------------------------------------------# #----------------------------------------------------------#
# Defining config path # Defining config path
config_path='/etc/nginx/nginx.conf' config_path='/usr/local/hestia/nginx-system/etc/nginx/nginx.conf'
# Defining keys # Defining keys
keys="worker_processes |worker_connections |send_timeout" keys="worker_processes |worker_connections |send_timeout"

View File

@@ -48,13 +48,13 @@ conf=$(grep "DOMAIN='$domain'" "$USER_DATA/web.conf")
parse_object_kv_list "$conf" parse_object_kv_list "$conf"
# Purge nginx FastCGI cache # Purge nginx FastCGI cache
if [ -d "/var/cache/nginx/micro/$domain" ]; then if [ -d "/usr/local/hestia/nginx-system/var/cache/nginx/micro/$domain" ]; then
rm -rf /var/cache/nginx/micro/$domain/* rm -rf /usr/local/hestia/nginx-system/var/cache/nginx/micro/$domain/*
fi fi
# Purge nginx proxy cache # Purge nginx proxy cache
if [ -d "/var/cache/nginx/$domain" ]; then if [ -d "/usr/local/hestia/nginx-system/var/cache/nginx/$domain" ]; then
rm -rf /var/cache/nginx/$domain/* rm -rf /usr/local/hestia/nginx-system/var/cache/nginx/$domain/*
fi fi
#----------------------------------------------------------# #----------------------------------------------------------#

View File

@@ -32,11 +32,8 @@ source $HESTIA/func/syshealth.sh
source_conf "$HESTIA/conf/hestia.conf" source_conf "$HESTIA/conf/hestia.conf"
# Define mail user # Define mail user
if [ "$MAIL_SYSTEM" = 'exim4' ]; then export MAIL_USER="exim"
export MAIL_USER="Debian-exim"
else
export MAIL_USER="exim"
fi
#----------------------------------------------------------# #----------------------------------------------------------#
# Verifications # # Verifications #

View File

@@ -30,11 +30,8 @@ source $HESTIA/func/syshealth.sh
source_conf "$HESTIA/conf/hestia.conf" source_conf "$HESTIA/conf/hestia.conf"
# Define mail user # Define mail user
if [ "$MAIL_SYSTEM" = 'exim4' ]; then export MAIL_USER="exim"
export MAIL_USER="Debian-exim"
else
export MAIL_USER="exim"
fi
#----------------------------------------------------------# #----------------------------------------------------------#
# Verifications # # Verifications #

View File

@@ -25,10 +25,14 @@ send_email_report() {
email=$(echo "$email" | cut -f 2 -d "'") email=$(echo "$email" | cut -f 2 -d "'")
tmpfile=$(mktemp) tmpfile=$(mktemp)
subj="$(hostname): $PROXY_SYSTEM restart failed" subj="$(hostname): $PROXY_SYSTEM restart failed"
nginx -t >> $tmpfile 2>&1 /usr/local/hestia/nginx-system/sbin/nginx -t >> $tmpfile 2>&1
if [ "$1" == "DO_RESTART" ]; then if [ "$1" == "DO_RESTART" ]; then
service "$PROXY_SYSTEM" restart >> $tmpfile 2>&1 if [ "$PROXY_SYSTEM" = "nginx" ]; then
service nginx-system restart >> $tmpfile 2>&1
else
service "$PROXY_SYSTEM" restart >> $tmpfile 2>&1
fi
fi fi
cat "$tmpfile" | $SENDMAIL -s "$subj" "$email" cat "$tmpfile" | $SENDMAIL -s "$subj" "$email"
if [ "$DEBUG_MODE" = "true" ]; then if [ "$DEBUG_MODE" = "true" ]; then

View File

@@ -73,11 +73,11 @@ for service in $service_list; do
$BIN/v-stop-firewall $BIN/v-stop-firewall
$BIN/v-update-firewall $BIN/v-update-firewall
elif [ "$restart" = "ssl" ] && [ "$service" = "nginx" ]; then elif [ "$restart" = "ssl" ] && [ "$service" = "nginx" ]; then
service $service upgrade >> $log 2>&1 service nginx-system upgrade >> $log 2>&1
elif [ -z "$restart" -o "$restart" = "no" ] && [ \ elif [ -z "$restart" -o "$restart" = "no" ] && [ \
"$service" = "nginx" -o \ "$service" = "nginx" -o \
"$service" = "httpd" -o \ "$service" = "httpd" -o \
"$service" = "exim4" -o \ "$service" = "exim" -o \
"$service" = "dovecot" -o \ "$service" = "dovecot" -o \
"$service" = "bind9" -o \ "$service" = "bind9" -o \
"$service" = "named" -o \ "$service" = "named" -o \
@@ -86,10 +86,19 @@ for service in $service_list; do
"$service" = "proftpd" -o \ "$service" = "proftpd" -o \
"$service" = "ssh" -o \ "$service" = "ssh" -o \
"$service" = "fail2ban" ]; then "$service" = "fail2ban" ]; then
systemctl reload-or-restart "$service" >> $log 2>&1 if [ "$service" = "nginx" ]; then
systemctl reload-or-restart nginx-system >> $log 2>&1
else
systemctl reload-or-restart "$service" >> $log 2>&1
fi
else else
systemctl reset-failed "$service" >> $log 2>&1 if [ "$service" = "nginx" ]; then
systemctl restart "$service" >> $log 2>&1 systemctl reset-failed nginx-system >> $log 2>&1
systemctl restart nginx-system >> $log 2>&1
else
systemctl reset-failed "$service" >> $log 2>&1
systemctl restart "$service" >> $log 2>&1
fi
fi fi
# Check the result of the service restart and report whether it failed. # Check the result of the service restart and report whether it failed.

View File

@@ -643,7 +643,7 @@ if [ "$mail" != 'no' ] && [ -n "$MAIL_SYSTEM" ]; then
mkdir /usr/local/hestia/ssl/mail mkdir /usr/local/hestia/ssl/mail
fi fi
# Add domain SSL configuration to exim4 # Add domain SSL configuration to exim
# Cleanup broken symlinks # Cleanup broken symlinks
find /usr/local/hestia/ssl/mail -xtype l -delete find /usr/local/hestia/ssl/mail -xtype l -delete

View File

@@ -47,7 +47,11 @@ for service in $service_list; do
if [ "$service" = "iptables" ]; then if [ "$service" = "iptables" ]; then
$BIN/v-stop-firewall $BIN/v-stop-firewall
else else
systemctl stop "$service" if [ "$service" = "nginx" ]; then
systemctl stop nginx-system
else
systemctl stop "$service"
fi
result=$? result=$?
if [ "$result" -ne 0 ]; then if [ "$result" -ne 0 ]; then
$BIN/v-log-action "system" "Error" "System" "Service failed to stop (Name: $service)." $BIN/v-log-action "system" "Error" "System" "Service failed to stop (Name: $service)."

282
bin/v-system-nginx-migrate Executable file
View File

@@ -0,0 +1,282 @@
#!/opt/brepo/ruby33/bin/ruby
# info: utility to prepare existing server with hestiacp to use new alternative nginx
# options: COMMAND
#
# example: v-system-nginx-migrate migrate-nginx
#
# Commands:
# migratenginx - move old nginx configs to the new port and path
#
#
#------------------------------------------#
# Variables & Functions #
#------------------------------------------#
# Argument definition
v_command = ARGV[0]
require "/usr/local/hestia/func_ruby/global_options"
load_ruby_options_defaults
$HESTIA = load_hestia_default_path_from_env
require "main"
require "modules"
require 'json' unless defined?(JSON)
require 'fileutils'
require 'time'
require 'pathname'
def copy_nginx_files(src_root, dest_root)
FileUtils.mkdir_p(dest_root)
Dir.foreach(src_root) do |entry|
next if entry == '.' || entry == '..'
next if entry == 'modules' || entry == 'modules-enabled'
src_path = File.join(src_root, entry)
dest_path = File.join(dest_root, entry)
if File.directory?(src_path)
FileUtils.mkdir_p(dest_path)
copy_nginx_files(src_path, dest_path)
else
FileUtils.cp(src_path, dest_path)
end
end
end
# Function to log and execute migration stages
def log_migrate_stage(stage)
log_file = '/usr/local/hestia/log/bunkerweb_migrate_stages.log'
# Check if the stage has already been recorded
if File.exist?(log_file) && File.readlines(log_file).any? { |line| line.strip == stage }
hestia_print_error_message_to_cli "Stage #{stage} already completed, skipping."
return
end
# Execute the stage block
begin
yield
# Record the successful stage
File.open(log_file, 'a') { |f| f.puts stage }
hestia_print_info_message_to_cli "Stage #{stage} completed."
rescue => e
hestia_print_error_message_to_cli "Stage #{stage} failed: #{e.message}"
exit 1
end
end
# Function to parse and migrate nginx.conf from /usr/local/hestia/nginx-system/etc/nginx/nginx.conf
def migrate_nginx_config_from_file(source_path)
return false unless File.exist?(source_path)
hestia_print_info_message_to_cli "Processing nginx config from: #{source_path}"
content = File.read(source_path)
original_content = content.dup
modified = false
# Replace all paths starting with /var/ to /usr/local/hestia/nginx-system/var/
# This pattern matches any absolute path that starts with /var/ anywhere in the line
content = content.gsub(/\/var\//, '/usr/local/hestia/nginx-system/var/')
# Replace pid path from /run/nginx.pid to /run/nginx-system.pid
content = content.gsub(/pid\s+\S+/) { |match| match.gsub('/run/nginx.pid', '/run/nginx-system.pid') }
if content != original_content
File.write(source_path, content)
hestia_print_info_message_to_cli "Updated config: #{source_path}"
modified = true
end
modified
end
def parse_listen(line)
# Попытка найти IP:port
m = line.match(/^\s*listen\s+([^\s:]+):(\d+)/i)
return [m[1], m[2]] if m
# Если только порт после listen
m = line.match(/^\s*listen\s+(\d+);?\s*$/i)
return [nil, m[1]] if m
nil
end
# Helper function to parse and replace ports in listen directives using temp placeholders
def parse_and_replace_listen_directive(line, proxy_port, proxy_ssl_port)
return line unless line.match?(/\blisten\b/i)
new_line = line.dup
# Define target ports (always migrate to these values regardless of input)
target_http_port = '8078'
target_ssl_port = '8079'
result = parse_listen(line)
return line unless result
ip, port = result
if port == proxy_port
if ip
new_line.gsub!("#{ip}:#{port}", "#{ip}:#{target_http_port}")
else
new_line.gsub!(port, target_http_port)
end
elsif port == proxy_ssl_port
if ip
new_line.gsub!("#{ip}:#{port}", "#{ip}:#{target_ssl_port}")
else
new_line.gsub!(port, target_ssl_port)
end
end
new_line
end
hestia_check_privileged_user
load_global_bash_variables "/etc/hestiacp/hestia.conf"
if $HESTIA.nil?
hestia_print_error_message_to_cli "Can't find HESTIA base path"
exit 1
end
load_global_bash_variables "#{$HESTIA}/conf/hestia.conf"
#------------------------------------------#
# Verifications #
#------------------------------------------#
check_args 1, ARGV, "COMMAND"
# Perform verification if read-only mode is enabled
check_hestia_demo_mode
#------------------------------------------#
# Action #
#------------------------------------------#
case v_command.to_sym
when :migratenginx
log_migrate_stage('nstage0') do
# Create backup of /etc/nginx with timestamp
timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
backup_root = "/etc/nginx_backup_#{timestamp}"
FileUtils.mkdir_p(backup_root)
src_root = '/etc/nginx'
dest_root = backup_root
# Custom copy function to handle symlinks in conf.d/domains
def copy_with_symlinks(src, dest)
Dir.foreach(src) do |entry|
next if entry == '.' || entry == '..'
src_path = File.join(src, entry)
dest_path = File.join(dest, entry)
if File.symlink?(src_path)
# Check if symlink is inside conf.d/domains
if src_path.include?(File.join('conf.d', 'domains'))
# Resolve the target of the symlink
target_path = File.readlink(src_path)
# Resolve relative symlink paths
unless Pathname.new(target_path).absolute?
target_path = File.expand_path(target_path, File.dirname(src_path))
end
if File.exist?(target_path) && File.file?(target_path)
content = File.read(target_path)
new_file_name = "#{entry}_content.conf"
new_file_path = File.join(dest, new_file_name)
File.write(new_file_path, content)
end
else
# Preserve the symlink as is
FileUtils.mkdir_p(File.dirname(dest_path))
target = File.readlink(src_path)
FileUtils.ln_s(target, dest_path)
end
elsif File.directory?(src_path)
FileUtils.mkdir_p(dest_path)
copy_with_symlinks(src_path, dest_path)
else
FileUtils.cp(src_path, dest_path)
end
end
end
copy_with_symlinks(src_root, dest_root)
end
log_migrate_stage('nstage2') do
unless system('yum install -y nginx-system')
hestia_print_error_message_to_cli "Failed to install nginx-system via yum"
log_event E_ARGS, $ARGUMENTS
exit 1
end
end
log_migrate_stage('nstage3') do
FileUtils.rm_f Dir.glob('/usr/local/hestia/nginx-system/etc/nginx/conf.d/*.conf')
src_root = '/etc/nginx'
dest_root = '/usr/local/hestia/nginx-system/etc/nginx'
copy_nginx_files(src_root, dest_root)
end
log_migrate_stage('nstage4') do
# Find all files under the nginx-system directory
nginx_conf_dir = '/usr/local/hestia/nginx-system/etc/nginx'
Dir.glob(File.join(nginx_conf_dir, '**', '*')).each do |path|
hestia_print_info_message_to_cli "stage 4 processing file #{path}"
next if File.directory?(path)
content = File.read(path)
new_content = content.gsub(/(?<!\/usr\/local\/hestia\/nginx-system)\/etc\/nginx/, '/usr/local/hestia/nginx-system/etc/nginx')
if new_content != content
hestia_print_info_message_to_cli "Changed path to config in file #{path}"
File.open(path, 'w') { |f| f.write(new_content) }
end
end
end
log_migrate_stage('nstage4.1') do
# Parse nginx.conf file to replace paths
if migrate_nginx_config_from_file("/usr/local/hestia/nginx-system/etc/nginx/nginx.conf")
hestia_print_info_message_to_cli "Completed migration of nginx.conf paths"
else
hestia_print_error_message_to_cli "Warning: Could not migrate nginx.conf from #{File.expand_path('/usr/local/hestia/nginx-system/etc/nginx/nginx.conf')}"
end
end
log_migrate_stage('nstage7') do
if system('/usr/local/hestia/bin/v-ext-modules enable update_module')
output = IO.popen("/usr/local/hestia/bin/v-ext-modules state update_module json").read
begin
parsed = JSON.parse(output)
if parsed.is_a?(Array) && parsed.first && parsed.first['STATE'] == 'enabled'
system('/usr/local/hestia/bin/v-ext-modules-run update_module synctemplates')
else
hestia_print_error_message_to_cli "update_module not enabled after enable command"
exit 1
end
rescue JSON::ParserError => e
hestia_print_error_message_to_cli "Failed to parse JSON from state command: #{e.message}"
exit 1
end
else
hestia_print_error_message_to_cli "Failed to enable update_module"
exit 1
end
end
log_migrate_stage('stage9') do
# Delete all contents inside /etc/nginx
FileUtils.rm_rf Dir.glob('/etc/nginx/*')
# Stop nginx service
system('systemctl stop nginx')
system('systemctl disable nginx')
# Start nginx-system service
system('systemctl enable nginx-system')
system('systemctl start nginx-system')
end
else
hestia_print_error_message_to_cli "unknown command (use migratenginx)"
log_event E_ARGS, $ARGUMENTS
exit 1
end
exit 0

View File

@@ -86,7 +86,11 @@ if [ -n "$old_ip" ]; then
# Updating PROXY # Updating PROXY
if [ -n "$PROXY_SYSTEM" ]; then if [ -n "$PROXY_SYSTEM" ]; then
cd /etc/$PROXY_SYSTEM/$pconfd if [ "$PROXY_SYSTEM" = "nginx" ]; then
cd /usr/local/hestia/nginx-system/etc/nginx/$pconfd
else
cd /etc/$PROXY_SYSTEM/$pconfd
fi
if [ -e "$old_ip.conf" ]; then if [ -e "$old_ip.conf" ]; then
mv $old_ip.conf $new_ip.conf mv $old_ip.conf $new_ip.conf
sed -i "s/$old_ip/$new_ip/g" $new_ip.conf sed -i "s/$old_ip/$new_ip/g" $new_ip.conf
@@ -95,7 +99,11 @@ if [ -n "$old_ip" ]; then
# Updating WEB # Updating WEB
if [ -n "$WEB_SYSTEM" ]; then if [ -n "$WEB_SYSTEM" ]; then
cd /etc/$WEB_SYSTEM/$confd if [ "$WEB_SYSTEM" = "nginx" ]; then
cd /usr/local/hestia/nginx-system/etc/nginx/$confd
else
cd /etc/$WEB_SYSTEM/$confd
fi
if [ -e "$old_ip.conf" ]; then if [ -e "$old_ip.conf" ]; then
mv $old_ip.conf $new_ip.conf mv $old_ip.conf $new_ip.conf
@@ -161,8 +169,8 @@ for ip in $ips; do
prefixlen="$(ip -d -j addr show | jq --arg IP "$ip" -r '.[].addr_info[] | if .local == $IP then .prefixlen else empty end')" prefixlen="$(ip -d -j addr show | jq --arg IP "$ip" -r '.[].addr_info[] | if .local == $IP then .prefixlen else empty end')"
netmask="$(convert_cidr "$prefixlen")" netmask="$(convert_cidr "$prefixlen")"
$BIN/v-add-sys-ip "$ip" "$netmask" "$interface" $BIN/v-add-sys-ip "$ip" "$netmask" "$interface"
elif [ -e "/etc/nginx/conf.d/$ip.conf" ]; then elif [ -e "/usr/local/hestia/nginx-system/etc/nginx/conf.d/$ip.conf" ]; then
process_http2_directive "/etc/nginx/conf.d/$ip.conf" process_http2_directive "/usr/local/hestia/nginx-system/etc/nginx/conf.d/$ip.conf"
fi fi
done done

View File

@@ -90,6 +90,25 @@ check_result $? "Web restart failed" > /dev/null
$BIN/v-restart-proxy "$restart" $BIN/v-restart-proxy "$restart"
check_result $? "Proxy restart failed" > /dev/null check_result $? "Proxy restart failed" > /dev/null
# Execute bunkerweb_module if it's enabled
module_state=$($BIN/v-ext-modules state bunkerweb_module json | jq -r '.[0].STATE')
if [ "$module_state" = "enabled" ] && [ "$BUNKERWEB" = "yes" ]; then
BUNKW_DIR=$HOMEDIR/$user/conf/web/$domain/ssl/bunkerweb
if [ ! -d "$BUNKW_DIR" ]; then
mkdir -p "$BUNKW_DIR"
chmod 0755 "$BUNKW_DIR"
fi
cp -f "$HOMEDIR/$user/conf/web/$domain/ssl/$domain.crt" "$BUNKW_DIR/"
cp -f "$HOMEDIR/$user/conf/web/$domain/ssl/$domain.key" "$BUNKW_DIR/"
cp -f "$HOMEDIR/$user/conf/web/$domain/ssl/$domain.pem" "$BUNKW_DIR/"
if [ -e "$HOMEDIR/$user/conf/web/$domain/ssl/$domain.ca" ]; then
cp -f "$HOMEDIR/$user/conf/web/$domain/ssl/$domain.ca" "$BUNKW_DIR/"
fi
chown root:nginx "$BUNKW_DIR"/*
chmod 0640 "$BUNKW_DIR"/*
$BIN/v-ext-modules-run bunkerweb_module updssl "$domain" "$BUNKW_DIR/$domain.pem" "$BUNKW_DIR/$domain.key"
fi
# Logging # Logging
$BIN/v-log-action "$user" "Info" "Web" "SSL certificate updated (Domain: $domain)." $BIN/v-log-action "$user" "Info" "Web" "SSL certificate updated (Domain: $domain)."
log_event "$OK" "$EVENT" log_event "$OK" "$EVENT"

View File

@@ -47,6 +47,35 @@ export default defineConfig({
copyright: copyright:
"Copyright © 2019-present Hestia Control Panel и некоторые RPM based компоненты принадлежат bayrepo", "Copyright © 2019-present Hestia Control Panel и некоторые RPM based компоненты принадлежат bayrepo",
}, },
search: {
provider: "local",
options: {
placeholder: "Поиск по документации",
minMatchCharLength: 1,
threshold: 0.2,
distance: 5000,
keys: ["title", "content", "headers"],
tokenize: (text) => {
const tokens = [];
const chineseChars = text.match(/[\u4e00-\u9fa5]/g) || [];
const englishWords = text.match(/[a-zA-Z0-9]+/g) || [];
return [...chineseChars, ...englishWords];
},
translations: {
button: { buttonText: "Поиск по документации" },
modal: {
noResultsText: "Ничего не найдено",
resetButtonTitle: "Сбросить поиск",
footer: {
selectText: "выбрать",
navigateText: "переключить",
closeText: "закрыть",
},
},
},
},
},
}, },
}); });
@@ -60,6 +89,7 @@ function nav() {
link: "/docs/introduction/getting-started.md", link: "/docs/introduction/getting-started.md",
activeMatch: "/docs/", activeMatch: "/docs/",
}, },
{ text: "Разработчики", link: "/team.md" },
{ {
text: `v${version}`, text: `v${version}`,
items: [ items: [

View File

@@ -0,0 +1,125 @@
<template>
<div class="CopyToClipboardInput" v-bind="$attrs">
<input
type="text"
class="CopyToClipboardInput-input"
:value="value"
@focus="selectText"
readonly
/>
<button
type="button"
class="CopyToClipboardInput-button"
@click="copyToClipboard"
title="Copy to clipboard"
>
Copy
</button>
</div>
</template>
<script setup>
import { ref } from "vue";
defineProps({
value: {
type: String,
required: true,
},
});
const selectText = (event) => {
if (window.innerWidth > 768) {
const inputElement = event.target;
inputElement.select();
}
};
const copyToClipboard = async (event) => {
const button = event.currentTarget;
const inputValue = button.previousElementSibling.value;
try {
const copyMethod = navigator.clipboard
? () => navigator.clipboard.writeText(inputValue)
: () => {
const textarea = document.createElement("textarea");
textarea.value = inputValue;
document.body.appendChild(textarea);
textarea.select();
document.execCommand("copy");
document.body.removeChild(textarea);
};
await copyMethod();
if (navigator.vibrate) navigator.vibrate(50);
button.style.transform = "scale(0.95)";
button.textContent = "Copied!";
setTimeout(() => {
button.style.transform = "";
button.textContent = "Copy";
}, 1000);
} catch (err) {
button.textContent = "Copy failed";
console.error("Failed to copy:", err);
setTimeout(() => {
button.textContent = "Copy";
}, 1000);
}
};
</script>
<style scoped>
.CopyToClipboardInput {
display: flex;
gap: 10px;
}
.CopyToClipboardInput-input {
font-size: 0.9em;
font-family: monospace;
flex-grow: 1;
border: 1px solid var(--vp-c-border);
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
background-color: var(--vp-c-bg);
padding: 8px 13px;
&:focus {
border-color: var(--vp-button-brand-bg);
}
@media (max-width: 640px) {
font-size: 16px;
-webkit-tap-highlight-color: transparent;
}
}
.CopyToClipboardInput-button {
font-size: 14px;
border-radius: 4px;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
color: var(--vp-button-brand-text);
min-width: 73px;
font-weight: 600;
padding: 5px 10px;
background-color: var(--vp-button-brand-bg);
transition: all 0.2s ease;
user-select: none;
&:hover {
background-color: var(--vp-button-brand-hover-bg);
}
@media (max-width: 640px) {
min-width: 68px;
padding: 8px 12px;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
&:active {
transform: scale(0.96);
}
}
}
</style>

View File

@@ -1,204 +1,250 @@
<script>
export default {
props: {
languages: {
required: true,
selected: "en",
},
items: {
required: true,
},
},
data() {
return {
pageloader: false,
hestia_wget:
"wget https://raw.githubusercontent.com/bayrepo/hestiacp-rpm/refs/heads/rhel-version/install/hst-install.sh",
hestia_wget_devel:
"wget https://dev.brepo.ru/bayrepo/hestiacp/raw/branch/master/install/hst-install.sh",
hestia_install: "sudo bash hst-install.sh",
installStr: "",
};
},
methods: {
getOptionString(item) {
if (item.textField) {
return item.selected ? `${item.param} '${item.text}'` : "";
}
if (item.selectField) {
return item.selected ? `${item.param} '${item.text}'` : "";
}
return `${item.param}${item.selected ? " yes" : " no"}`;
},
generateString() {
const installStr = this.items.map(this.getOptionString).filter(Boolean);
this.installStr = `${this.hestia_install} ${installStr.join(" ")}`;
this.$refs.dialog.showModal();
},
closeDialog(e) {
if (e.target === this.$refs.dialogClose || e.target === this.$refs.dialog) {
this.$refs.dialog.close();
}
},
checkNeedEnabled(e) {
if (e.target.value != "") {
let id = e.target.getAttribute("target");
if (!document.getElementById(id).checked) {
document.getElementById(id).click();
}
}
},
toggleOption(e) {
if (e.target.checked) {
let conflicts = e.target.getAttribute("conflicts");
if (conflicts) {
if (document.getElementById(conflicts).checked) {
document.getElementById(conflicts).click();
}
}
let depends = e.target.getAttribute("depends");
if (depends) {
if (!document.getElementById(depends).checked) {
document.getElementById(depends).click();
}
}
}
},
copyToClipboard(text, button) {
navigator.clipboard.writeText(text).then(
() => {
button.textContent = "Copied!";
setTimeout(() => {
button.textContent = "Copy";
}, 1000);
},
(err) => {
console.error("Could not copy to clipboard:", err);
}
);
},
},
};
</script>
<template> <template>
<div class="container"> <div class="InstallOptions">
<div class="grid"> <div class="container">
<div class="form-group" v-for="item in items"> <div class="output-card">
<div class="form-check u-mb10"> <h1 class="u-text-center">Инструкция по установке для RHEL</h1>
<input
@change="toggleOption"
type="checkbox"
class="form-check-input"
v-model="item.selected"
:value="item.value"
:id="item.id"
:conflicts="item.conflicts"
:depends="item.depends"
/>
<label :for="item.id">{{ item.id }}</label>
</div>
<template v-if="item.textField || item.selectField">
<label class="form-label" :for="'input-' + item.id">{{ item.desc }}</label>
</template>
<template v-else>
<p>{{ item.desc }}</p>
</template>
<div v-if="item.textField">
<input
@change="checkNeedEnabled"
type="text"
class="form-control"
v-model="item.text"
:target="item.id"
:id="'input-' + item.id"
:type="'+item.type+'"
/>
</div>
<div v-if="item.selectField">
<select class="form-select" v-model="item.text" :id="'input-' + item.id">
<option v-for="language in languages" :value="language.value" :key="language.value">
{{ language.text }}
</option>
</select>
</div>
</div>
</div>
<div class="u-text-center u-mb10">
<button @click="generateString" class="form-submit" type="button">Submit</button>
</div>
<dialog ref="dialog" class="modal" @click="closeDialog">
<button class="modal-close" @click="closeDialog" type="button" ref="dialogClose">
Close
</button>
<div ref="dialogContent" class="modal-content">
<h1 class="modal-heading">Installation instructions</h1>
<p class="u-mb10"> <p class="u-mb10">
Log in to your server as root, either directly or via SSH: Войдите на свой сервер как root, напрямую или через SSH: ssh root@ваш.сервер, и загрузите
<code>ssh root@your.server</code> and download the installation script: установочный скрипт:
</p> </p>
<div class="u-pos-relative"> <CopyToClipboardInput
<input class="u-mb10"
type="text" value="wget https://raw.githubusercontent.com/bayrepo/hestiacp-rpm/refs/heads/rhel-version/install/hst-install.sh"
class="form-control u-monospace u-mb10" style="font-size: 1.2em"
v-model="hestia_wget" />
readonly <p class="u-mb10">Для загрузки разработочной версии используйте:</p>
/> <CopyToClipboardInput
<button class="u-mb10"
class="button-positioned" value="wget https://dev.brepo.ru/bayrepo/hestiacp/raw/branch/master/install/hst-install.sh"
@click="copyToClipboard(hestia_wget, $event.target)" style="font-size: 1.2em"
type="button" />
title="Copy to Clipboard" <p class="u-mb10">
> Убедитесь, что вы вошли на сервер как <code>root</code>. Настройте параметры в полях ниже,
Copy затем скопируйте и выполните готовую команду в терминале сервера:
</button> </p>
</div> <CopyToClipboardInput class="u-mb10" :value="installCommand" style="font-size: 1.2em" />
<p class="u-mb10">For downloading devel version use:</p>
<div class="u-pos-relative">
<input
type="text"
class="form-control u-monospace u-mb10"
v-model="hestia_wget_devel"
readonly
/>
<button
class="button-positioned"
@click="copyToClipboard(hestia_wget_devel, $event.target)"
type="button"
title="Copy to Clipboard"
>
Copy
</button>
</div>
<p class="u-mb10">Then run the following command:</p>
<div class="u-pos-relative">
<textarea class="form-control u-min-height100" v-model="installStr" readonly />
<button
class="button-positioned"
@click="copyToClipboard(installStr, $event.target)"
type="button"
title="Copy to Clipboard"
>
Copy
</button>
</div>
</div> </div>
</dialog> <h2 class="u-text-center">Настройка компонентов: выберите необходимые для установки</h2>
<h3 class="u-text-center">
<i class="fa-solid fa-triangle-exclamation"></i> Примечание: Рекомендуется выполнить полную
настройку здесь, иначе вам придётся вводить данные в терминале! (Пункты 1.2.3.4.5
обязательны)
</h3>
<ul class="option-list">
<li
v-for="option in options"
:key="option.flag"
:class="{
'option-item': true,
'is-active': selectedOptions[option.flag].enabled,
'is-clickable': !option.type || !selectedOptions[option.flag].enabled,
}"
>
<div class="option-header">
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
:id="option.flag"
v-model="selectedOptions[option.flag].enabled"
:conflicts="option.conflicts"
:depends="option.depends"
@change="toggleOption"
/>
<label :for="option.flag" @click.stop>{{ option.label }}</label>
</div>
</div>
<div class="option-description" v-html="option.description"></div>
<div v-if="option.type === 'text'" class="option-content">
<input
class="form-control"
type="text"
:id="`${option.flag}-input`"
v-model="selectedOptions[option.flag].value"
:placeholder="option.default"
@input="checkNeedEnabled(option.flag, $event)"
/>
</div>
<div v-if="option.type === 'select'" class="option-content">
<select
class="form-select"
:id="`${option.flag}-input`"
v-model="selectedOptions[option.flag].value"
@change="checkNeedEnabled(option.flag, $event)"
>
<option v-for="opt in option.options" :key="opt.value" :value="opt.value">
{{ opt.label }}
</option>
</select>
</div>
</li>
</ul>
</div>
</div> </div>
</template> </template>
<script setup>
import { ref, watchEffect } from "vue";
import CopyToClipboardInput from "./CopyToClipboardInput.vue";
const { options } = defineProps({
options: {
type: Array,
required: true,
default: () => [],
},
});
const selectedOptions = ref({});
options.forEach((option) => {
const isPortOrLang = ["port", "lang"].includes(option.flag);
selectedOptions.value[option.flag] = {
enabled: isPortOrLang || option.default === "yes",
value: option.default !== "yes" && option.default !== "no" ? option.default : null,
};
});
const checkNeedEnabled = (flag, event) => {
if (event.target.value !== "" && event.target.value !== null) {
if (!selectedOptions.value[flag].enabled) {
selectedOptions.value[flag].enabled = true;
}
}
};
const toggleOption = (event) => {
const checkbox = event.target;
const flag = checkbox.id;
const conflicts = checkbox.getAttribute("conflicts");
const depends = checkbox.getAttribute("depends");
if (checkbox.checked) {
if (conflicts) {
const conflictFlag = conflicts;
if (selectedOptions.value[conflictFlag]?.enabled) {
selectedOptions.value[conflictFlag].enabled = false;
}
}
if (depends) {
const dependFlag = depends;
if (!selectedOptions.value[dependFlag]?.enabled) {
selectedOptions.value[dependFlag].enabled = true;
}
}
}
};
const installCommand = ref("bash hst-install.sh");
watchEffect(() => {
let cmd = "sudo bash hst-install.sh";
const quoteshellarg = (str) => {
if (!str) return "''";
return `'${str.replace(/'/g, "'\\''")}'`;
};
for (const [key, { enabled, value }] of Object.entries(selectedOptions.value)) {
const opt = options.find((o) => o.flag === key);
if (!opt.type || opt.type === "checkbox") {
if (enabled !== (opt.default === "yes")) {
cmd += ` --${key} ${enabled ? "yes" : "no"}`;
}
} else if (enabled && value !== opt.default) {
const value_quoted = quoteshellarg(value);
cmd += ` --${key} ${value_quoted}`;
}
}
installCommand.value = cmd;
});
</script>
<style scoped> <style scoped>
.InstallOptions {
padding: 0 24px;
@media (min-width: 640px) {
padding: 0 48px;
}
@media (min-width: 960px) {
padding: 0 72px;
}
}
:root {
--text-color-light: #282828;
--text-color-dark: #f0f0f0;
}
h1 {
font-size: 32px;
font-weight: bold;
text-align: center;
margin-bottom: 30px;
color: var(--text-color-light);
}
h2 {
font-size: 24px;
font-weight: 600;
margin-bottom: 25px;
color: var(--text-color-light);
}
h3 {
font-size: 20px;
font-weight: 500;
margin-bottom: 20px;
color: #b7236a;
font-weight: bold;
}
@media (max-width: 640px) {
h1 {
font-size: 20px;
}
h2 {
font-size: 18px;
}
h3 {
font-size: 16px;
}
}
p {
font-size: 16px;
font-weight: bold;
line-height: 1.6;
margin-bottom: 15px;
color: var(--text-color-light);
}
a {
color: var(--vp-button-brand-active-bg);
text-decoration: none;
}
a:visited {
color: var(--vp-button-brand-active-bg);
}
a:hover {
color: var(--vp-button-brand-hover-bg);
}
a:active {
color: var(--vp-button-brand-hover-bg);
}
.container { .container {
margin: 0px auto; display: flex;
flex-direction: column;
margin: 0 auto;
max-width: 1152px; max-width: 1152px;
} }
.grid { .output-card {
background-color: var(--vp-c-bg-alt);
border-radius: 10px;
padding: 30px;
margin-top: 40px;
margin-bottom: 40px;
@media (min-width: 640px) {
padding: 30px 50px;
}
}
.option-list {
display: grid; display: grid;
grid-gap: 20px; grid-gap: 23px;
margin-top: 30px; margin-bottom: 50px;
margin-bottom: 30px;
@media (min-width: 640px) { @media (min-width: 640px) {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
@@ -208,17 +254,87 @@ export default {
grid-template-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
} }
} }
.form-group { .option-item {
font-size: 0.9em; font-size: 0.9em;
border-radius: 10px; border-radius: 10px;
padding: 15px 20px; border: 2px solid transparent;
padding: 10px 20px;
background-color: var(--vp-c-bg-alt); background-color: var(--vp-c-bg-alt);
transition: border-color 0.2s;
&:hover {
border-color: var(--vp-button-brand-hover-bg);
}
&.is-active {
border-color: var(--vp-button-brand-active-bg);
}
}
.option-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.option-icon {
padding: 5px;
margin-right: -5px;
& i {
opacity: 0.7;
}
&:hover i {
opacity: 1;
}
}
.option-description {
font-size: 13px;
line-height: 1.5;
margin-bottom: 10px;
padding-bottom: 8px;
}
.option-description pre {
background-color: var(--vp-c-bg, #f5f5f5);
border: 1px solid var(--vp-c-border, #e0e0e0);
border-radius: 8px;
padding: 12px 16px;
overflow-x: auto;
margin: 12px 0;
font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", monospace;
font-size: 13px;
line-height: 1.5;
}
.option-description code {
font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", monospace;
font-size: 13px;
background-color: var(--vp-c-bg, #f0f0f0);
padding: 2px 6px;
border-radius: 4px;
}
.option-description pre code {
background-color: transparent;
padding: 0;
font-size: 13px;
}
@media (prefers-color-scheme: dark) {
.option-description pre {
background-color: #2d2d2d;
border-color: #444;
}
.option-description code {
background-color: #2d2d2d;
}
}
.option-content {
margin-top: 5px;
margin-bottom: 5px;
} }
.form-label { .form-label {
display: inline-block; display: inline-block;
margin-left: 2px; font-size: 16px;
line-height: 1.5;
padding-bottom: 5px; padding-bottom: 5px;
text-transform: capitalize;
} }
.form-control { .form-control {
font-size: 0.9em; font-size: 0.9em;
@@ -242,7 +358,7 @@ export default {
border: 1px solid var(--vp-c-border); border: 1px solid var(--vp-c-border);
border-radius: 4px; border-radius: 4px;
background-color: var(--vp-c-bg); background-color: var(--vp-c-bg);
padding: 5px 10px; padding: 6px;
width: 100%; width: 100%;
&:hover { &:hover {
@@ -254,100 +370,36 @@ export default {
} }
} }
.form-check { .form-check {
flex-grow: 1;
position: relative; position: relative;
padding-left: 20px; padding-left: 25px;
margin-left: 3px;
min-height: 24px;
& label { & label {
font-size: 16px;
font-weight: 600; font-weight: 600;
display: block;
line-height: 1.6;
&:hover {
cursor: pointer;
}
} }
} }
.form-check-input { .form-check-input {
cursor: pointer;
position: absolute; position: absolute;
width: 15px;
height: 15px;
margin-top: 5px; margin-top: 5px;
margin-left: -20px; margin-left: -25px;
}
.form-submit {
border: 1px solid transparent;
display: inline-block;
font-weight: 600;
transition: color 0.25s, border-color 0.25s, background-color 0.25s;
border-radius: 20px;
font-size: 16px;
padding: 10px 20px;
background-color: var(--vp-button-brand-bg);
border-color: var(--vp-button-brand-border);
color: var(--vp-button-brand-text);
&:hover {
background-color: var(--vp-button-brand-hover-bg);
border-color: var(--vp-button-brand-hover-border);
color: var(--vp-button-brand-hover-text);
}
&:active {
background-color: var(--vp-button-brand-active-bg);
border-color: var(--vp-button-brand-active-border);
color: var(--vp-button-brand-active-text);
}
}
.button-positioned {
position: absolute;
right: 1px;
top: 1px;
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
color: var(--vp-c-brand);
font-weight: 600;
padding: 6px 10px;
background-color: var(--vp-c-bg);
}
.modal {
position: fixed;
border-radius: 10px;
border: 1px solid var(--vp-c-border);
box-shadow: 0 8px 40px 0 rgb(0 0 0 / 35%);
padding: 0;
&::backdrop {
background-color: rgb(0 0 0 / 50%);
}
}
.modal-close {
position: absolute;
top: 10px;
right: 15px;
font-weight: 600;
color: var(--vp-c-brand);
}
.modal-content {
padding: 30px;
}
.modal-heading {
font-weight: 600;
font-size: 1.3em;
text-align: center;
margin-bottom: 15px;
}
code {
background-color: var(--vp-c-bg-alt);
border-radius: 3px;
padding: 2px 5px;
} }
.u-mb10 { .u-mb10 {
margin-bottom: 10px !important; margin-bottom: 10px !important;
} }
.u-min-height100 {
min-height: 100px;
}
.u-text-center { .u-text-center {
text-align: center !important; text-align: center !important;
} }
.u-monospace { .is-clickable {
font-family: monospace !important; cursor: pointer;
}
.u-pos-relative {
position: relative !important;
} }
</style> </style>

View File

@@ -1,29 +0,0 @@
<template>
<form class="InstallForm" id="form">
<div class="InstallOptionsSection">
<slot name="list" />
</div>
<cite
>Based on: <a href="https://github.com/gabizz/hestiacp-scriptline-generator">@gabizz</a> and
<a href="https://github.com/turbopixel/HestiaCP-Command-Creator">@turbopixel</a></cite
>
</form>
</template>
<style scoped>
.InstallForm {
margin: 0.55em 0;
padding: 0 1em;
line-height: 1.5;
}
cite {
font-size: small;
margin: 0.55em 0;
display: block;
text-align: center;
& a {
color: var(--vp-c-txt-1) !important;
}
}
</style>

View File

@@ -6,23 +6,6 @@
<style scoped> <style scoped>
.InstallPage { .InstallPage {
line-height: 1.5; line-height: 1;
}
.InstallPage :deep(.container) {
display: flex;
flex-direction: column;
margin: 0 auto;
max-width: 1152px;
}
.InstallPage :deep(a) {
font-weight: 500;
color: var(--vp-c-brand);
text-decoration-style: dotted;
transition: color 0.25s;
}
.InstallPage :deep(a:hover) {
color: var(--vp-c-brand-dark);
} }
</style> </style>

View File

@@ -1,42 +0,0 @@
<template>
<header class="InstallPageTitle">
<div class="container">
<h1>
<slot name="title"></slot>
</h1>
<p v-if="$slots.lead" class="lead">
<slot name="lead" />
</p>
</div>
</header>
</template>
<style scoped>
.InstallPageTitle {
padding: 0 24px;
background-color: var(--vp-c-bg-alt);
}
.InstallPageTitle h1 {
margin: 0.75em 0;
font-size: 2rem;
font-weight: 700;
line-height: inherit;
}
@media (min-width: 640px) {
.InstallPageTitle {
padding: 0 48px;
}
.InstallPageTitle h1 {
font-size: 2.5rem;
}
}
@media (min-width: 960px) {
.InstallPageTitle {
padding: 0 64px;
}
}
</style>

View File

@@ -0,0 +1,82 @@
<template>
<header class="PageHeader">
<div class="container">
<h1>
<slot name="title"></slot>
</h1>
<h2 class="subtitle">
<slot name="subtitle"></slot>
</h2>
<div v-if="$slots.aside" class="aside-container">
<slot name="aside"></slot>
</div>
</div>
</header>
</template>
<style scoped>
:root {
--text-color-light: #282828;
--text-color-dark: var(--el-color-primary-light-9);
}
.container {
display: flex;
margin: 0 auto;
text-align: center;
align-items: center;
justify-content: center;
max-width: 1260px;
color: var(--text-color-light);
}
.aside-container {
display: flex;
margin-left: auto;
margin-bottom: 15px;
}
.subtitle {
margin: -0.5em 0 1.2em 0;
font-size: 1.2rem;
font-weight: 700;
line-height: 1.6;
color: inherit;
text-align: center;
max-width: 800px;
}
.subtitle :deep(a) {
color: var(--vp-c-brand);
text-decoration: none;
transition: color 0.3s;
}
.subtitle :deep(a):hover {
color: #d8036a;
}
.PageHeader {
padding: 0 24px;
background-color: var(--vp-c-bg-alt);
h1 {
margin: 0.75em 0;
font-size: 2rem;
font-weight: 700;
line-height: inherit;
@media (max-width: 640px) {
font-size: 20px;
}
}
@media (min-width: 640px) {
padding: 0 48px;
}
@media (min-width: 960px) {
padding: 0 64px;
}
}
</style>

View File

@@ -6,11 +6,12 @@ import './styles/base.css';
import './styles/vars.css'; import './styles/vars.css';
import FeaturePage from './components/FeaturePage.vue'; import FeaturePage from './components/FeaturePage.vue';
import InstallPage from './components/InstallPage.vue'; import InstallPage from './components/InstallPage.vue';
import PageHeader from './components/PageHeader.vue';
export default { export default {
...Theme, ...Theme,
enhanceApp({ app }) { enhanceApp({ app }) {
app.component('FeaturePage', FeaturePage); app.component('FeaturePage', FeaturePage);
app.component('InstallPage', InstallPage); app.component('InstallPage', InstallPage);
app.component('PageHeader', PageHeader);
}, },
}; };

View File

@@ -2,10 +2,14 @@
--vp-c-brand-lighter: #ff558f; --vp-c-brand-lighter: #ff558f;
--vp-c-brand-light: #da4778; --vp-c-brand-light: #da4778;
--vp-c-brand: #b7236a; --vp-c-brand: #b7236a;
--vp-c-brand-dark: #9a1d5a; --vp-c-brand-dark: #e2237f;
--vp-c-brand-darker: #781746; --vp-c-brand-darker: #c00b62;
--vp-home-hero-name-color: transparent; --vp-home-hero-name-color: transparent;
--vp-home-hero-name-background: linear-gradient(120deg, #d83182, #633288, #41d1ff); --vp-home-hero-name-background: linear-gradient(120deg, #d83182, #633288, #41d1ff);
--vp-button-brand-bg: #b7236a;
--vp-button-brand-hover-bg: #d8036a;
--vp-button-brand-active-bg: #e7197d;
--vp-c-brand-1: #b7236a;
} }
.dark { .dark {
@@ -15,4 +19,8 @@
--vp-c-brand: #ff558f; --vp-c-brand: #ff558f;
--vp-c-brand-dark: #da4778; --vp-c-brand-dark: #da4778;
--vp-c-brand-darker: #b7236a; --vp-c-brand-darker: #b7236a;
--vp-button-brand-bg: #da4778;
--vp-button-brand-hover-bg: #b7236a;
--vp-c-brand-1: #da4778;
--vp-code-block-bg: #282c34;
} }

View File

@@ -1,43 +0,0 @@
export const languages = [
{ text: 'Arabic', value: 'ar' },
{ text: 'Armenian', value: 'hy' },
{ text: 'Azerbaijani', value: 'az' },
{ text: 'Bengali', value: 'bn' },
{ text: 'Bosnian', value: 'bs' },
{ text: 'Bulgarian', value: 'bg' },
{ text: 'Catalan', value: 'ca' },
{ text: 'Croatian', value: 'hr' },
{ text: 'Czech', value: 'cs' },
{ text: 'Danish', value: 'da' },
{ text: 'Dutch', value: 'nl' },
{ text: 'English', value: 'en' },
{ text: 'Finnish', value: 'fi' },
{ text: 'French', value: 'fr' },
{ text: 'Georgian', value: 'ka' },
{ text: 'German', value: 'de' },
{ text: 'Greek', value: 'el' },
{ text: 'Hungarian', value: 'hu' },
{ text: 'Indonesian', value: 'id' },
{ text: 'Italian', value: 'it' },
{ text: 'Japanese', value: 'ja' },
{ text: 'Korean', value: 'ko' },
{ text: 'Kurdish Sorani', value: 'ku' },
{ text: 'Norwegain', value: 'no' },
{ text: 'Persian', value: 'fa' },
{ text: 'Polish', value: 'pl' },
{ text: 'Portuguese', value: 'pt' },
{ text: 'Portuguese (Brasil)', value: 'pt-br' },
{ text: 'Romanian', value: 'ro' },
{ text: 'Russian', value: 'ru' },
{ text: 'Serbian', value: 'sr' },
{ text: 'Simplified Chinese (China)', value: 'zh-cn' },
{ text: 'Slovak', value: 'sk' },
{ text: 'Spanish', value: 'es' },
{ text: 'Swedish', value: 'sv' },
{ text: 'Thai', value: 'th' },
{ text: 'Traditional Chinese (Taiwan)', value: 'zh-tw' },
{ text: 'Turkish', value: 'tr' },
{ text: 'Ukrainian', value: 'uk' },
{ text: 'Urdu', value: 'ur' },
{ text: 'Vietnamese', value: 'vi' },
];

View File

@@ -1,207 +1,253 @@
export const options = [ export const options = [
{ {
name: ' --port', flag: 'port',
id: 'port', label: 'Порт управления',
param: '--port', description: 'Установите порт HTTPS панели Hestia. По умолчанию: 8083. Обязательный параметр!',
desc: 'Изменить порт Hestia', type: 'text',
selected: true, required: 'true',
text: '8083', default: '8083',
textField: true,
}, },
{ {
name: ' --lang', flag: 'lang',
id: 'language', label: 'Язык интерфейса панели',
param: '--lang', description:
desc: 'ISO 639-1 codes', 'Выберите язык веб-интерфейса. Если не выбирать, система будет использовать английский язык по умолчанию.',
selected: true, type: 'select',
default: 'en', default: 'en',
selectField: true, options: [
text: 'en', { label: 'Shqip (Albanian)', value: 'sq' },
{ label: 'العربية (Arabic)', value: 'ar' },
{ label: 'Հայերեն (Armenian)', value: 'hy' },
{ label: 'Azərbaycan (Azerbaijani)', value: 'az' },
{ label: 'বাংলা (Bengali)', value: 'bn' },
{ label: 'Bosanski (Bosnian)', value: 'bs' },
{ label: 'Български (Bulgarian)', value: 'bg' },
{ label: 'Català (Catalan)', value: 'ca' },
{ label: 'Hrvatski (Croatian)', value: 'hr' },
{ label: 'Čeština (Czech)', value: 'cs' },
{ label: 'Dansk (Danish)', value: 'da' },
{ label: 'Nederlands (Dutch)', value: 'nl' },
{ label: 'English', value: 'en' },
{ label: 'Suomi (Finnish)', value: 'fi' },
{ label: 'Français (French)', value: 'fr' },
{ label: 'ქართული (Georgian)', value: 'ka' },
{ label: 'Deutsch (German)', value: 'de' },
{ label: 'Ελληνικά (Greek)', value: 'el' },
{ label: 'Magyar (Hungarian)', value: 'hu' },
{ label: 'Bahasa Indonesia (Indonesian)', value: 'id' },
{ label: 'Italiano (Italian)', value: 'it' },
{ label: '日本語 (Japanese)', value: 'ja' },
{ label: '한국어 (Korean)', value: 'ko' },
{ label: 'Kurdî (Sorani Kurdish)', value: 'ku' },
{ label: 'Norsk (Norwegian)', value: 'no' },
{ label: 'فارسی (Persian)', value: 'fa' },
{ label: 'Polski (Polish)', value: 'pl' },
{ label: 'Português (Portuguese)', value: 'pt' },
{ label: 'Português do Brasil (Brazilian Portuguese)', value: 'pt-br' },
{ label: 'Română (Romanian)', value: 'ro' },
{ label: 'Русский (Russian)', value: 'ru' },
{ label: 'Српски (Serbian)', value: 'sr' },
{ label: '简体中文 (Chinese Simplified)', value: 'zh-cn' },
{ label: 'Slovenčina (Slovak)', value: 'sk' },
{ label: 'Español (Spanish)', value: 'es' },
{ label: 'Svenska (Swedish)', value: 'sv' },
{ label: 'ไทย (Thai)', value: 'th' },
{ label: '繁體中文 (Chinese Traditional)', value: 'zh-tw' },
{ label: 'Türkçe (Turkish)', value: 'tr' },
{ label: 'Українська (Ukrainian)', value: 'uk' },
{ label: 'اردو (Urdu)', value: 'ur' },
{ label: 'Tiếng Việt (Vietnamese)', value: 'vi' },
],
}, },
{ {
name: ' --hostname', flag: 'hostname',
id: 'hostname', label: 'Имя хоста сервера',
param: '--hostname', description:
desc: 'Установить имя хоста', 'Укажите веб-имя хоста сервера. Следуйте формату: demo.example.com. Обязательный параметр!',
selected: false, type: 'text',
text: '', default: '',
textField: true,
}, },
{ {
name: ' --email', flag: 'email',
id: 'email', label: 'Электронная почта',
param: '--email', description:
desc: 'Установить почту адмнистратора', 'Введите email администратора для уведомлений об异常 конфигурации сервера. Обязательный параметр!',
selected: false, type: 'text',
text: '', default: '',
textField: true,
}, },
{ {
name: ' --password', flag: 'password',
id: 'password', label: 'Пароль',
param: '--password', description:
desc: 'Установить пароль администратора', 'Установите пароль для учетной записи администратора. Если не указать, будет сгенерирован случайный пароль.',
selected: false, type: 'text',
text: '', default: '',
textField: true,
}, },
{ {
name: ' --apache', flag: 'nopublicip',
id: 'apache', label: 'Принудительно использовать локальный IP',
param: '--apache', description:
desc: 'Веб сервер с поддержкой .htaccess', 'Использовать внутренний/локальный IP-адрес сервера в качестве адреса привязки, не пытаться получить публичный IP. Подходит для внутренних сетей, NAT-сетей, локальной разработки и тестирования, где не требуется публичный доступ.',
selected: true, default: 'no',
},
{ name: ' --phpfpm', id: 'phpfpm', param: '--phpfpm', desc: 'Установить PHP-FPM.', selected: true },
{
name: ' --multiphp',
id: 'multiphp',
param: '--multiphp',
desc: 'Разрешить использовать несколько PHP версий.',
selected: true,
}, },
{ {
name: ' --vsftpd', flag: 'apache',
id: 'vsftpd', label: 'Apache2',
param: '--vsftpd', description:
desc: 'Легковесный, минималистичный и безопасный FTP сервер.', 'Apache2 — мощный, высококонфигурируемый и широко поддерживаемый веб-сервер с открытым исходным кодом. Используется для хостинга веб-сайтов, поддержки веб-приложений, балансировки нагрузки и кэширования.',
selected: true, default: 'yes',
conflicts: 'proftpd',
}, },
{ {
name: ' --proftpd', flag: 'phpfpm',
id: 'proftpd', label: 'PHP-FPM',
param: '--proftpd', description: 'PHP-FPM — это менеджер процессов FastCGI для выполнения PHP-скриптов.',
desc: 'Продвинутый, модульный FTP сервер с поддержкой LDAP.', default: 'yes',
selected: false,
conflicts: 'vsftpd',
}, },
{ {
name: ' --named', flag: 'multiphp',
id: 'named', label: 'MultiPHP',
param: '--named', description:
desc: 'Собственный DNS сервер.', 'При включении по умолчанию устанавливается PHP 8.2, а также появляется возможность установки нескольких версий PHP 7.4-8.5 в панели. При отключении устанавливается только стандартная версия PHP.',
selected: true, default: 'yes',
}, },
{ {
name: ' --mysql', flag: 'vsftpd',
id: 'mariadb', label: 'VSFTPD',
param: '--mysql', description:
desc: 'Ответление MySQL с дополнительными усовершенствованиями.', 'VSFTPD — это безопасный, быстрый и стабильный FTP-сервер. Особенно подходит для Linux-систем. Обеспечивает безопасную и быструю передачу файлов между сервером и пользователями.',
selected: true, default: 'yes',
conflicts: 'mysql8',
}, },
{ {
name: ' --mysql-classic', flag: 'proftpd',
id: 'mysql8', label: 'ProFTPD',
param: '--mysql-classic', description:
desc: 'База данных с открытым исходным кодом', 'ProFTPD — это продвинутый модульный FTP-сервер. Поддерживает аутентификацию и авторизацию пользователей через LDAP.',
selected: false, default: 'no',
conflicts: 'mariadb',
}, },
{ {
name: ' --postgresql', flag: 'named',
id: 'postgresql', label: 'BIND',
param: '--postgresql', description:
desc: 'База данных с открытым исходным кодом', 'BIND (Berkeley Internet Name Domain) — популярное DNS-программное обеспечение. Обязательный параметр для серверных кластеров, требующих настройки собственного DNS.',
selected: false, default: 'yes',
}, },
{ {
name: ' --exim', flag: 'mysql',
id: 'exim', label: 'MariaDB',
param: '--exim', description:
desc: 'Позволить отправлять почту по SMTP.', 'MariaDB — это ветка MySQL, предоставляющая функции, совместимые с MySQL, а также некоторые дополнительные возможности и улучшения.',
selected: true, default: 'yes',
}, },
{ {
name: ' --dovecot', flag: 'mysql-classic',
id: 'dovecot', label: 'MySQL 8',
param: '--dovecot', description:
desc: 'Принимать почту и отправлять по IMAP/POP3.', 'MySQL 8 предлагает простоту настройки, управления и масштабирования. Обеспечивает повышенную безопасность, высокую доступность в пределах одного региона или с резервированием и гарантирует 99.99% уровень обслуживания (SLA).',
selected: true, default: 'no',
depends: 'exim',
}, },
{ {
name: ' --sieve', flag: 'postgresql',
id: 'sieve', label: 'PostgreSQL',
param: '--sieve', description:
desc: 'Управлять собственными фильтрами почты.', 'PostgreSQL — мощная объектно-реляционная система управления базами данных с открытым исходным кодом. Широко используется в финансовых услугах, обрабатывающей промышленности, розничной торговле, логистике и многих других областях.',
selected: false, default: 'no',
depends: 'dovecot',
}, },
{ {
name: ' --clamav', flag: 'exim',
id: 'clamav', label: 'Exim',
param: '--clamav', description:
desc: 'Сканировать почту на вирусы.', 'Exim — это агент передачи сообщений (MTA), используемый для маршрутизации, доставки и приема сообщений электронной почты. Позволяет отправлять электронные письма через SMTP из сети или от локальных программ.',
selected: true, default: 'yes',
depends: 'exim',
},
{
name: ' --usemirrorclamav',
id: 'usemirrorclamav',
param: '--usemirrorclamav',
desc: 'Использовать русское зеркало баз данных ClamaV',
selected: false,
depends: 'clamav',
}, },
{ {
name: ' --spamassassin', flag: 'dovecot',
id: 'spamassassin', label: 'Dovecot',
param: '--spamassassin', description:
desc: 'Фильтровать почту на спам.', 'Dovecot — это серверное программное обеспечение с открытым исходным кодом для IMAP и POP3. Обеспечивает почтовые услуги для Linux-систем. Известен своей безопасностью, простотой использования, скоростью и низким потреблением ресурсов.',
selected: true, default: 'yes',
depends: 'exim',
}, },
{ {
name: ' --firewall', flag: 'sieve',
id: 'firewall', label: 'Правила Sieve',
param: '--firewall', description:
desc: 'Управлять фаерволом.', 'Включает поддержку правил фильтрации почты (Sieve), позволяя пользователям настраивать автоматическую сортировку, обработку спама, автоматические ответы и другие правила. Требует Dovecot и Exim.',
selected: true, default: 'no',
}, },
{ {
name: ' --nopublicip', flag: 'clamav',
id: 'nopublicip', label: 'ClamAV',
param: '--nopublicip', description:
desc: 'Использовать локальный IP сервера', 'ClamAV — это кроссплатформенное программное обеспечение с открытым исходным кодом для обеспечения безопасности электронной почты. Используется для обнаружения вирусов в электронных письмах и других файлах.',
selected: false, default: 'yes',
}, },
{ {
name: ' --uselocalphp', flag: 'usemirrorclamav',
id: 'uselocalphp', label: 'Российское зеркало ClamAV',
param: '--uselocalphp', description:
desc: 'Использовать PHP из текущего репозитория HestiaCP', 'Использовать российское зеркало для обновления вирусных баз ClamAV. Подходит для серверов, расположенных в России или соседних регионах, где загрузка с официального источника медленная или нестабильная. Базы будут загружаться с repo.brepo.ru, возможна задержка в несколько часов.',
selected: false, default: 'no',
}, },
{ {
name: ' --usemirrorclamav', flag: 'spamassassin',
id: 'usemirrorclamav', label: 'SpamAssassin',
param: '--usemirrorclamav', description:
desc: 'Использовать clamav с базами скачанными с зеркала в России', 'SpamAssassin — это инструмент с открытым исходным кодом для идентификации и фильтрации спама. Он анализирует заголовки, тело и информацию об отправителе письма, применяя ряд правил для оценки того, является ли письмо спамом.',
selected: false, default: 'yes',
}, },
{ {
name: ' --fail2ban', flag: 'firewall',
id: 'fail2ban', label: 'Системный брандмауэр',
param: '--fail2ban', description:
desc: 'Предотвращение Bruteforce атак на SSH, Email, FTP, БД.', 'Использовать firewalld / nftables в качестве системного брандмауэра.<br>\n<br>⚠️ Внимание: Если после установки вы не можете получить доступ к панели через публичный IP (например, доступ только через localhost), это обычно означает, что порт 8083 не открыт в брандмауэре.\n\nРешение:\n\n<pre><code>firewall-cmd --permanent --add-port=8083/tcp\nfirewall-cmd --reload</code></pre>\n\nПри использовании nftables:\n\n<pre><code>nft add rule inet filter input tcp dport 8083 accept</code></pre>\n\nРекомендуется оставить брандмауэр включенным для повышения безопасности, возможна совместная работа с Fail2Ban для защиты от брутфорса.',
selected: true, default: 'yes',
}, },
{ {
name: ' --quota', flag: 'bunkerweb',
id: 'quota', label: 'Межсетевой экран WAF Bunkerweb',
param: '--quota', description:
desc: 'Использовать квоты на пользовательские аккаунты.', 'Bunkerweb — это межсетевой экран веб-приложений (WAF) с открытым исходным кодом, обеспечивающий защиту на основе основных правил OWASP, черные/белые списки IP, ограничение скорости, HTTP-заголовки безопасности и другие функции безопасности.',
selected: false, default: 'no',
}, },
{ name: ' --api', id: 'api', param: '--api', desc: 'Activate API.', selected: true },
{ {
name: ' --interactive', flag: 'uselocalphp',
id: 'interactive', label: 'Использовать официальную сборку PHP от Hestia',
param: '--interactive', description:
desc: 'Интерактивная установка.', 'Использовать версию PHP из официального репозитория HestiaCP вместо стандартного источника операционной системы. При включении пакеты PHP будут загружаться из репозитория brepo, что обеспечивает единообразие версий и совместимость.',
selected: true, default: 'no',
},
{
flag: 'fail2ban',
label: 'Fail2Ban',
description:
'Fail2Ban может автоматически обнаруживать и блокировать пользователей, использующих недействительные данные аутентификации, например, при попытках взлома SSH, FTP и т.д.',
default: 'yes',
},
{
flag: 'quota',
label: 'Дисковые квоты',
description:
'Установить квоты на дисковое пространство для системных пользователей, ограничивая использование дискового пространства.',
default: 'no',
},
{
flag: 'api',
label: 'Hestia API',
description:
'Включить внутренний API Hestia, позволяющий другим системам или сервисам взаимодействовать с панелью.',
default: 'yes',
},
{
flag: 'interactive',
label: 'Интерактивная установка',
description:
'Включить интерактивный режим во время установки, чтобы пользователь мог отвечать на запросы и выбирать опции.',
default: 'yes',
},
{
flag: 'force',
label: 'Принудительная установка',
description:
'Принудительно перезаписать любые существующие конфигурации и файлы во время установки. В дистрибутивах семейства RHEL рекомендуется включить этот параметр. Перезаписывает конфигурацию администратора по умолчанию.',
default: 'no',
}, },
{ name: ' --force', id: 'force', param: '--force', desc: 'Принудительная установка.', selected: false },
]; ];

View File

@@ -3,46 +3,33 @@ export const projectManagers = [
{ {
avatar: 'https://www.github.com/ScIT-Raphael.png', avatar: 'https://www.github.com/ScIT-Raphael.png',
name: 'Raphael Schneeberger 🇨🇭', name: 'Raphael Schneeberger 🇨🇭',
title: 'Project Founder', title: 'Основатель проекта',
links: [{ icon: 'github', link: 'https://github.com/ScIT-Raphael' }], links: [{ icon: 'github', link: 'https://github.com/ScIT-Raphael' }],
}, },
{ {
avatar: 'https://www.github.com/jaapmarcus.png', avatar: 'https://www.github.com/jaapmarcus.png',
name: 'Jaap Marcus 🇳🇱', name: 'Jaap Marcus 🇳🇱',
links: [ links: [{ icon: 'github', link: 'https://github.com/jaapmarcus' }],
{ icon: 'github', link: 'https://github.com/jaapmarcus' },
{ icon: 'twitter', link: 'https://twitter.com/jaapmarcus' },
],
}, },
{ {
avatar: 'https://www.github.com/Lupul.png', avatar: 'https://www.github.com/Lupul.png',
name: 'Robert Zollner 🇷🇴', name: 'Robert Zollner 🇷🇴',
links: [{ icon: 'github', link: 'https://github.com/Lupul' }], links: [{ icon: 'github', link: 'https://github.com/Lupul' }],
}, },
{
avatar: 'https://www.github.com/krismkenn.png',
name: 'Kristan Kenney 🇨🇦',
links: [{ icon: 'github', link: 'https://github.com/krismkenn' }],
},
]; ];
const globeIcon = {
svg: '🌐',
};
/** @type {import("vitepress").DefaultTheme.TeamMember[]} */ /** @type {import("vitepress").DefaultTheme.TeamMember[]} */
export const teamMembers = [ export const teamMembers = [
{ {
avatar: 'https://www.github.com/jakobbouchard.png', avatar: 'https://www.github.com/imjuniper.png',
name: 'Jakob Bouchard 🇨🇦', name: 'Juniper Bouchard 🇨🇦',
title: 'Developer',
org: 'Prosomo',
orgLink: 'https://prosomo.com',
links: [ links: [
{ icon: 'github', link: 'https://github.com/jakobbouchard' }, { icon: 'github', link: 'https://github.com/imjuniper' },
{ icon: 'linkedin', link: 'https://linkedin.com/in/jakobbouchard' }, { icon: globeIcon, link: 'https://imjuniper.fyi' },
{
icon: {
svg: '<svg role="img" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><title>Website</title><path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" /></svg>',
},
link: 'https://jakobbouchard.dev',
},
], ],
}, },
{ {
@@ -56,11 +43,6 @@ export const teamMembers = [
name: 'divinity76 🇳🇴', name: 'divinity76 🇳🇴',
links: [{ icon: 'github', link: 'https://github.com/divinity76' }], links: [{ icon: 'github', link: 'https://github.com/divinity76' }],
}, },
{
avatar: 'https://www.github.com/istiak101.png',
name: 'istiak101 🇧🇩',
links: [{ icon: 'github', link: 'https://github.com/istiak101' }],
},
{ {
avatar: 'https://www.github.com/Pleskan.png', avatar: 'https://www.github.com/Pleskan.png',
name: 'Anton Pleskanovskyy 🇺🇦', name: 'Anton Pleskanovskyy 🇺🇦',
@@ -69,6 +51,33 @@ export const teamMembers = [
{ {
avatar: 'https://www.github.com/AlecRust.png', avatar: 'https://www.github.com/AlecRust.png',
name: 'Alec Rust 🇬🇧', name: 'Alec Rust 🇬🇧',
links: [{ icon: 'github', link: 'https://github.com/AlecRust' }], links: [
{ icon: 'github', link: 'https://github.com/AlecRust' },
{ icon: globeIcon, link: 'https://www.alecrust.com/' },
],
},
{
avatar: 'https://github.com/ioannidesalex.png',
name: 'Alexandros Ioannides 🇨🇾',
title: 'Технический директор (CTO)',
org: 'FocusNet',
orgLink: 'https://focus-net.net',
links: [
{ icon: 'github', link: 'https://github.com/ioannidesalex' },
{ icon: 'linkedin', link: 'https://www.linkedin.com/in/ioannidesa' },
],
},
];
/** @type {import("vitepress").DefaultTheme.TeamMember[]} */
export const teamRpm = [
{
avatar: 'https://www.github.com/bayrepo.png',
name: 'Alexey Berezhok 🇷🇺',
title: 'Сопровождающий RPM-версии',
desc: 'Отвечает за адаптацию и поддержку экосистем RHEL / Rocky / AlmaLinux.',
links: [
{ icon: 'github', link: 'https://github.com/bayrepo' },
{ icon: globeIcon, link: 'https://hestiadocs.brepo.ru' },
],
}, },
]; ];

View File

@@ -13,12 +13,12 @@ Hestia необходимо установить поверх новой уст
Если на VPS/KVM уже есть учетная запись администратора, либо удалите этот идентификатор администратора по умолчанию, либо используйте `--force`, чтобы продолжить установку. Дополнительные сведения см. в разделе «Выборочная установка» ниже. Если на VPS/KVM уже есть учетная запись администратора, либо удалите этот идентификатор администратора по умолчанию, либо используйте `--force`, чтобы продолжить установку. Дополнительные сведения см. в разделе «Выборочная установка» ниже.
::: :::
| | Минимум | Рекомендуется | | Компонент | Минимум | Рекомендуется |
| -------------------- | --------------------------------------------- | ------------------------------------ | | :--------------------------------------- | :------------------------------------------------------- | :----------------------------------------------------- |
| **ЦП** | 1 ядро, 64-разрядный | 4 ядра | | **ЦП** | 1 ядро, 64-разрядный | 4 ядра |
| **Память** | 1 ГБ (без SpamAssassin и ClamAV) | 4 ГБ | | **Память** | 1 ГБ (без SpamAssassin и ClamAV) | 4 ГБ |
| **Диск** | 10 ГБ HDD | 40 ГБ SSD | | **Диск** | 10 ГБ HDD | 40 ГБ SSD |
| **Операционная система** | Debian 10, 11 или 12<br>Ubuntu 20.04, 22.04 LTS | Последняя версия Debian <br> Последняя версия Ubuntu LTS | | **Операционная система (RPM)** | AlmaLinux 9<br>Rocky Linux 9 | Rocky Linux 9<br>AlmaLinux 9 |
::: warning ::: warning
Hestia работает только на процессорах AMD64 / x86_64 и ARM64 / aarch64. Также требуется 64-разрядная операционная система! Hestia работает только на процессорах AMD64 / x86_64 и ARM64 / aarch64. Также требуется 64-разрядная операционная система!
@@ -28,7 +28,6 @@ Hestia работает только на процессорах AMD64 / x86_64
### Поддерживаемые операционные системы ### Поддерживаемые операционные системы
- MSVSphere 9
- AlmaLinux 9 - AlmaLinux 9
- Rocky Linux 9 - Rocky Linux 9
@@ -79,37 +78,36 @@ bash hst-install.sh -h
Чтобы выбрать, какое программное обеспечение будет установлено, вы можете указать флаги в скрипте установки. Полный список параметров можно просмотреть ниже. Чтобы выбрать, какое программное обеспечение будет установлено, вы можете указать флаги в скрипте установки. Полный список параметров можно просмотреть ниже.
```bash ```bash
-a, --apache Install Apache [yes|no] default: yes -a, --apache Install Apache [yes | no] default: yes
-w, --phpfpm Install PHP-FPM [yes|no] default: yes -w, --phpfpm Install PHP-FPM [yes | no] default: yes
-o, --multiphp Install Multi-PHP [yes|no] default: no -o, --multiphp Install Multi-PHP [yes | no] default: no
-v, --vsftpd Install Vsftpd [yes|no] default: yes -v, --vsftpd Install Vsftpd [yes | no] default: yes
-j, --proftpd Install ProFTPD [yes|no] default: no -j, --proftpd Install ProFTPD [yes | no] default: no
-k, --named Install Bind [yes|no] default: yes -k, --named Install Bind [yes | no] default: yes
-m, --mysql Install MariaDB [yes|no] default: yes -m, --mysql Install MariaDB [yes | no] default: yes
-M, --mysql-classic Install MySQL 8 [yes|no] default: no -M, --mysql-classic Install MySQL 8 [yes | no] default: no
-g, --postgresql Install PostgreSQL [yes|no] default: no -g, --postgresql Install PostgreSQL [yes | no] default: no
-x, --exim Install Exim [yes|no] default: yes -x, --exim Install Exim [yes | no] default: yes
-z, --dovecot Install Dovecot [yes|no] default: yes -z, --dovecot Install Dovecot [yes | no] default: yes
-Z, --sieve Install Sieve [yes|no] default: no -Z, --sieve Install Sieve [yes | no] default: no
-c, --clamav Install ClamAV [yes|no] default: no -c, --clamav Install ClamAV [yes | no] default: no
-t, --spamassassin Install SpamAssassin [yes|no] default: yes -t, --spamassassin Install SpamAssassin [yes | no] default: yes
-i, --firewall Install firewalld [yes|no] default: yes -i, --firewall Install firewalld [yes | no] default: yes
-b, --fail2ban Install Fail2ban [yes|no] default: yes -b, --fail2ban Install Fail2ban [yes | no] default: yes
-q, --quota Filesystem Quota [yes|no] default: no -q, --quota Filesystem Quota [yes | no] default: no
-d, --api Activate API [yes|no] default: yes -d, --api Activate API [yes | no] default: yes
-r, --port Change Backend Port default: 8083 -r, --port Change Backend Port default: 8083
-l, --lang Default language default: en -l, --lang Default language default: en
-y, --interactive Interactive install [yes|no] default: yes -y, --interactive Interactive install [yes | no] default: yes
-I, --nopublicip Use local ip [yes|no] default: no -I, --nopublicip Use local ip [yes | no] default: no
-u, --uselocalphp Use PHP from local repo [yes|no] default: no -u, --uselocalphp Use PHP from local repo [yes | no] default: no
-C, --usemirrorclamav Use mirrored clamav [yes|no] default: no -C, --usemirrorclamav Use mirrored clamav [yes | no] default: no
-s, --hostname Set hostname -s, --hostname Set hostname
-e, --email Set admin email -e, --email Set admin email
-p, --password Set admin password -p, --password Set admin password
-R, --with-rpms Path to Hestia rpms -R, --with-rpms Path to Hestia rpms
-f, --force Force installation -f, --force Force installation
-h, --help Print this help -h, --help Print this help
``` ```
#### Пример #### Пример
@@ -149,4 +147,4 @@ bash hst-install.sh \
- `-I, --nopublicip` - если установлена данная опция, то установщик панели не получает внешний IP адрес сервера, где контрольная панель устанавливается (на случай если ваш сервер за NAT). Данную опцию можно включать, если у вас на сервере публичный IP или необходима внутрисетевая установка без внешнего доступа. - `-I, --nopublicip` - если установлена данная опция, то установщик панели не получает внешний IP адрес сервера, где контрольная панель устанавливается (на случай если ваш сервер за NAT). Данную опцию можно включать, если у вас на сервере публичный IP или необходима внутрисетевая установка без внешнего доступа.
- `-u, --uselocalphp` - включить установку пакетов PHP из того-же репозитория, откуда ставится HestiaCP RPM Edition. Если не включать данную опцию, то PHP пакеты будут установлены из репозитория Remi. Если включить, то будет произведена так называемая установка локальных пакетов. Есть отличия в способе упаковке Local PHP и Remi PHP. Если сервера требуется расширенный набор PHP из Remi репозитория, то данную опцию лучше отключить, т.е задать `--uselocalphp no`. Local PHP более простая сборка с меньшим числом поддерживаемых модулей PHP, но подходит для большинства конфигураций CMS и имеет [страницу настройки подключаемых модулей](/docs/extensions/local-php.md) - `-u, --uselocalphp` - включить установку пакетов PHP из того-же репозитория, откуда ставится HestiaCP RPM Edition. Если не включать данную опцию, то PHP пакеты будут установлены из репозитория Remi. Если включить, то будет произведена так называемая установка локальных пакетов. Есть отличия в способе упаковке Local PHP и Remi PHP. Если сервера требуется расширенный набор PHP из Remi репозитория, то данную опцию лучше отключить, т.е задать `--uselocalphp no`. Local PHP более простая сборка с меньшим числом поддерживаемых модулей PHP, но подходит для большинства конфигураций CMS и имеет [страницу настройки подключаемых модулей](/docs/extensions/local-php.md)

View File

@@ -24,13 +24,6 @@ yum update
sed -i "s/obscure yescrypt/obscure sha512/g" /etc/pam.d/common-password sed -i "s/obscure yescrypt/obscure sha512/g" /etc/pam.d/common-password
``` ```
### Конфигурация Exim4
```bash
rm -f /etc/exim4/exim4.conf.template
cp -f /usr/local/hestia/install/deb/exim/exim4.conf.4.94.template /etc/exim4/exim4.conf.template
```
### ProFTPD ### ProFTPD
Закомментируйте [строку 29](https://github.com/hestiacp/hestiacp/blob/1ff8a4e5207aae1e241954a83b7e8070bcdca788/install/deb/proftpd/proftpd.conf#L29) в `/etc/profpd/prodtpd.conf`. Закомментируйте [строку 29](https://github.com/hestiacp/hestiacp/blob/1ff8a4e5207aae1e241954a83b7e8070bcdca788/install/deb/proftpd/proftpd.conf#L29) в `/etc/profpd/prodtpd.conf`.

View File

@@ -112,11 +112,11 @@ fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
try_files $uri =404; try_files $uri =404;
fastcgi_pass %backend_lsnr%; fastcgi_pass %backend_lsnr%;
fastcgi_index index.php; fastcgi_index index.php;
include /etc/nginx/fastcgi_params; include /usr/local/hestia/nginx-system/usr/local/hestia/nginx-system/usr/local/hestia/nginx-system/usr/local/hestia/nginx-system/usr/local/hestia/nginx-system/usr/local/hestia/nginx-system/usr/local/hestia/nginx-system/usr/local/hestia/nginx-system/usr/local/hestia/nginx-system/usr/local/hestia/nginx-system/etc/nginx/fastcgi_params;
} }
``` ```
Добавьте следующие строки под `include /etc/nginx/fastcgi_params;`: Добавьте следующие строки под `include /usr/local/hestia/nginx-system/usr/local/hestia/nginx-system/etc/nginx/fastcgi_params;`:
```bash ```bash
include %home%/%user%/conf/web/%domain%/nginx.fastcgi_cache.conf*; include %home%/%user%/conf/web/%domain%/nginx.fastcgi_cache.conf*;

View File

@@ -1,17 +0,0 @@
---
aside: false
lastUpdated: false
---
# Donate to the project
By donating to Hestia, you help maintain server costs, development time, and more!
Here are our official donation platforms:
- [PayPal](https://www.paypal.com/donate/?cmd=_s-xclick&hosted_button_id=ST87LQH2CHGLA)
- Crypto
- Bitcoin: bc1q48jt5wg5jaj8g9zy7c3j03cv57j2m2u5anlutu
- Ethereum: 0xfF3Dd2c889bd0Ff73d8085B84A314FC7c88e5D51
- Binance: bnb1l4ywvw5ejfmsgjdcx8jn5lxj7zsun8ktfu7rh8
- Smart Chain: 0xfF3Dd2c889bd0Ff73d8085B84A314FC7c88e5D51

View File

@@ -15,22 +15,27 @@ hero:
- theme: brand - theme: brand
text: Начало работы text: Начало работы
link: /docs/introduction/getting-started link: /docs/introduction/getting-started
- theme: alt - theme: brand
text: RPM версия text: RPM версия
link: https://dev.brepo.ru/bayrepo/hestiacp link: https://dev.brepo.ru/bayrepo/hestiacp
- theme: alt - theme: brand
text: Оригинальная панель для Debian/Ubuntu text: Оригинал для Debian/Ubuntu
link: https://github.com/hestiacp/hestiacp link: https://github.com/hestiacp/hestiacp
features: features:
- icon: 🚀 - icon: 🚀
title: Быстро и легко обучиться title: Минималистичный опыт эксплуатации и обслуживания
details: Hestia создана с учетом простоты использования. Создать пользователя или веб-сайт так же просто, как нажать кнопку и заполнить пару полей, при этом расширенные функции легко доступны. details: Hestia разработана с учётом простоты использования. Создание пользователя или сайта так же просто, как нажать на кнопку и заполнить несколько полей. Расширенные функции также настраиваются очень легко.
link: /docs/introduction/getting-started link: /docs/introduction/getting-started
linkText: Документация linkText: Документация
- icon: ❤️
title: Сделано с душой и гордостью
details: Hestia — это开源 серверное программное обеспечение, созданное увлечёнными разработчиками со всего мира. Это означает, что код проверяется многими людьми, и каждый может внести свой вклад в его развитие!
link: /team
linkText: Команда
- icon: 🛠️ - icon: 🛠️
title: Мощная и расширяемая title: Расширяемая и мощная
details: Hestia расширяема и проста в освоении. Вы можете создавать свои собственные темы, веб-шаблоны, быстро устанавливать приложения и взаимодействовать с ней с помощью программного обеспечения сторонних производителей через ее API. details: Hestia расширяема и проста в освоении. Вы можете создавать свои собственные темы, веб-шаблоны, быстро устанавливать приложения и взаимодействовать с ней с помощью программного обеспечения сторонних производителей через её API.
link: /docs/contributing/development link: /docs/contributing/development
linkText: Помощь в разработке Hestia linkText: Помочь разработке Hestia
--- ---

View File

@@ -1,23 +1,54 @@
--- ---
layout: page layout: page
title: Install title: Установка панели
--- ---
<script setup> <script setup>
import InstallPageTitle from "./.vitepress/theme/components/InstallPageTitle.vue"; import PageHeader from './.vitepress/theme/components/PageHeader.vue';
import InstallOptions from "./.vitepress/theme/components/InstallOptions.vue"; import InstallOptions from './.vitepress/theme/components/InstallOptions.vue';
import InstallOptionsSection from "./.vitepress/theme/components/InstallOptionsSection.vue"; import InstallPage from './.vitepress/theme/components/InstallPage.vue';
import { options } from "./_data/options"; import { options } from './_data/options';
import { languages } from "./_data/languages";
</script> </script>
<InstallPage> <InstallPage>
<InstallPageTitle> <PageHeader>
<template #title>Генератор установочной команды</template> <template #title>Генератор конфигурации для быстрой установки панели управления сервером Hestia</template>
</InstallPageTitle> </PageHeader>
<InstallOptionsSection> <PageHeader>
<template #list> <template #aside><a class="header-button" href="./docs/introduction/getting-started#требования">Посмотреть требования к установке</a></template>
<InstallOptions :items="options" :languages="languages"></InstallOptions> </PageHeader>
</template> <InstallOptions :options="options"></InstallOptions>
</InstallOptionsSection>
</InstallPage> </InstallPage>
<style>
.header-button {
display: inline-block;
border: 1px solid transparent;
font-weight: 600;
transition: color 0.25s, border-color 0.25s, background-color 0.25s;
border-radius: 20px;
padding: 0 20px;
line-height: 38px;
font-size: 14px;
border-color: var(--vp-button-alt-border);
color: var(--vp-button-alt-text);
background-color: var(--vp-button-alt-bg);
&:hover {
border-color: var(--vp-button-alt-hover-border);
color: var(--vp-button-alt-hover-text);
background-color: var(--vp-button-alt-hover-bg);
}
}
</style>
<style>
.header-button {
color: white;
background-color: var(--vp-c-brand);
}
.header-button:hover {
color: white;
background-color: #d8036a;
}
</style>

6
docs/public/paypal.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,37 +1,38 @@
--- ---
layout: page layout: page
title: Команда
title: The Team
--- ---
<style>
.VPTeamPageSection {
margin-top: 50px !important;
}
</style>
<script setup> <script setup>
import { VPTeamPage, VPTeamPageTitle, VPTeamPageSection, VPTeamMembers } from "vitepress/theme"; import { VPTeamPage, VPTeamPageTitle, VPTeamPageSection, VPTeamMembers } from "vitepress/theme";
import { projectManagers, teamMembers } from "./_data/team"; import { projectManagers, teamMembers, teamRpm } from "./_data/team";
</script> </script>
<VPTeamPage>
<VPTeamPageTitle> <VPTeamPageTitle>
<template #title>The Team</template> <template #title>Команда</template>
<template #lead> <template #lead>
The development of Hestia is guided by an international team, some of whom have chosen to be featured below. Разработка Hestia осуществляется международной командой, некоторые из участников которой представлены ниже.
</template> </template>
</VPTeamPageTitle> </VPTeamPageTitle>
<VPTeamPageSection> <VPTeamPageSection>
<template #title>Project Managers</template> <template #title>Руководители проекта</template>
<template #members> <template #members>
<VPTeamMembers :members="projectManagers" /> <VPTeamMembers :members="projectManagers" />
</template> </template>
</VPTeamPageSection> </VPTeamPageSection>
<VPTeamPageSection> <VPTeamPageSection>
<template #title>Team Members</template> <template #title>Участники команды</template>
<template #members> <template #members>
<VPTeamMembers :members="teamMembers" /> <VPTeamMembers :members="teamMembers" />
</template> </template>
</VPTeamPageSection> </VPTeamPageSection>
<!-- <VPTeamPageSection> <VPTeamPageSection>
<template #title>Contributors ❤️</template> <template #title>Сопровождающий RPM-версии</template>
<template #members> <template #members>
<VPTeamMembers size="small" :members="featuredContributors" /> <VPTeamMembers :members="teamRpm" />
</template> </template>
</VPTeamPageSection> --> </VPTeamPageSection>
</VPTeamPage>

View File

@@ -383,7 +383,11 @@ add_web_config() {
if [[ "$TPLNM" =~ stpl$ ]]; then if [[ "$TPLNM" =~ stpl$ ]]; then
rm -f /etc/$1/$confd/domains/$domain.ssl.conf rm -f /etc/$1/$confd/domains/$domain.ssl.conf
ln -s $conf /etc/$1/$confd/domains/$domain.ssl.conf if [ "$1" = "nginx" ]; then
ln -s $conf /usr/local/hestia/nginx-system/etc/$1/$confd/domains/$domain.ssl.conf
else
ln -s $conf /etc/$1/$confd/domains/$domain.ssl.conf
fi
# Rename/Move extra SSL config files # Rename/Move extra SSL config files
find=$(find $HOMEDIR/$user/conf/web/*.$domain.org* 2> /dev/null) find=$(find $HOMEDIR/$user/conf/web/*.$domain.org* 2> /dev/null)
@@ -399,8 +403,13 @@ add_web_config() {
fi fi
done done
else else
rm -f /etc/$1/$confd/domains/$domain.conf if [ "$1" = "nginx" ]; then
ln -s $conf /etc/$1/$confd/domains/$domain.conf rm -f /usr/local/hestia/nginx-system/etc/$1/$confd/domains/$domain.conf
ln -s $conf /usr/local/hestia/nginx-system/etc/$1/$confd/domains/$domain.conf
else
rm -f /etc/$1/$confd/domains/$domain.conf
ln -s $conf /etc/$1/$confd/domains/$domain.conf
fi
# Rename/Move extra config files # Rename/Move extra config files
find=$(find $HOMEDIR/$user/conf/web/*.$domain.org* 2> /dev/null) find=$(find $HOMEDIR/$user/conf/web/*.$domain.org* 2> /dev/null)
for f in $find; do for f in $find; do
@@ -518,17 +527,29 @@ del_web_config() {
rm -f $legacyconf rm -f $legacyconf
# Remove old global includes file # Remove old global includes file
rm -f /etc/$1/$confd/hestia.conf if [ "$1" = "nginx" ]; then
rm -f /usr/local/hestia/nginx-system/etc/$1/$confd/hestia.conf
else
rm -f /etc/$1/$confd/hestia.conf
fi
fi fi
# Remove domain configuration files and clean up symbolic links # Remove domain configuration files and clean up symbolic links
rm -f "$conf" rm -f "$conf"
if [ -n "$WEB_SYSTEM" ] && [ "$WEB_SYSTEM" = "$1" ]; then if [ -n "$WEB_SYSTEM" ] && [ "$WEB_SYSTEM" = "$1" ]; then
rm -f "/etc/$WEB_SYSTEM/$confd/domains/$confname" if [ "$WEB_SYSTEM" = "nginx" ]; then
rm -f "/usr/local/hestia/nginx-system/etc/$WEB_SYSTEM/$confd/domains/$confname"
else
rm -f "/etc/$WEB_SYSTEM/$confd/domains/$confname"
fi
fi fi
if [ -n "$PROXY_SYSTEM" ] && [ "$PROXY_SYSTEM" = "$1" ]; then if [ -n "$PROXY_SYSTEM" ] && [ "$PROXY_SYSTEM" = "$1" ]; then
rm -f "/etc/$PROXY_SYSTEM/$confd/domains/$confname" if [ "$PROXY_SYSTEM" = "nginx" ]; then
rm -f "/usr/local/hestia/nginx-system/etc/$PROXY_SYSTEM/$confd/domains/$confname"
else
rm -f "/etc/$PROXY_SYSTEM/$confd/domains/$confname"
fi
fi fi
} }
@@ -868,7 +889,7 @@ add_mail_ssl_config() {
echo " ssl_key = <$HOMEDIR/$user/conf/mail/$domain/ssl/$domain.key" >> /etc/dovecot/conf.d/domains/$domain.conf echo " ssl_key = <$HOMEDIR/$user/conf/mail/$domain/ssl/$domain.key" >> /etc/dovecot/conf.d/domains/$domain.conf
echo "}" >> /etc/dovecot/conf.d/domains/$domain.conf echo "}" >> /etc/dovecot/conf.d/domains/$domain.conf
# Add domain SSL configuration to exim4 # Add domain SSL configuration to exim
ln -s $HOMEDIR/$user/conf/mail/$domain/ssl/$domain.pem $HESTIA/ssl/mail/$domain.crt ln -s $HOMEDIR/$user/conf/mail/$domain/ssl/$domain.pem $HESTIA/ssl/mail/$domain.crt
ln -s $HOMEDIR/$user/conf/mail/$domain/ssl/$domain.key $HESTIA/ssl/mail/$domain.key ln -s $HOMEDIR/$user/conf/mail/$domain/ssl/$domain.key $HESTIA/ssl/mail/$domain.key
fi fi
@@ -880,7 +901,7 @@ add_mail_ssl_config() {
echo " ssl_key = <$HOMEDIR/$user/conf/mail/$domain/ssl/$domain.key" >> /etc/dovecot/conf.d/domains/$domain.conf echo " ssl_key = <$HOMEDIR/$user/conf/mail/$domain/ssl/$domain.key" >> /etc/dovecot/conf.d/domains/$domain.conf
echo "}" >> /etc/dovecot/conf.d/domains/$domain.conf echo "}" >> /etc/dovecot/conf.d/domains/$domain.conf
# Add domain SSL configuration to exim4 # Add domain SSL configuration to exim
ln -s $HOMEDIR/$user/conf/mail/$domain/ssl/$domain.pem $HESTIA/ssl/mail/mail.$domain.crt ln -s $HOMEDIR/$user/conf/mail/$domain/ssl/$domain.pem $HESTIA/ssl/mail/mail.$domain.crt
ln -s $HOMEDIR/$user/conf/mail/$domain/ssl/$domain.key $HESTIA/ssl/mail/mail.$domain.key ln -s $HOMEDIR/$user/conf/mail/$domain/ssl/$domain.key $HESTIA/ssl/mail/mail.$domain.key
@@ -917,8 +938,16 @@ del_mail_ssl_config() {
# Remove SSL vhost configuration # Remove SSL vhost configuration
rm -f $HOMEDIR/$user/conf/mail/$domain/*.*ssl.conf rm -f $HOMEDIR/$user/conf/mail/$domain/*.*ssl.conf
rm -f /etc/$WEB_SYSTEM/$confd/domains/$WEBMAIL_ALIAS.$domain.ssl.conf if [ "$WEB_SYSTEM" = "nginx" ]; then
rm -f /etc/$PROXY_SYSTEM/$pconfd/domains/$WEBMAIL_ALIAS.$domain.ssl.conf rm -f /usr/local/hestia/nginx-system/etc/$WEB_SYSTEM/$confd/domains/$WEBMAIL_ALIAS.$domain.ssl.conf
else
rm -f /etc/$WEB_SYSTEM/$confd/domains/$WEBMAIL_ALIAS.$domain.ssl.conf
fi
if [ "$PROXY_SYSTEM" = "nginx" ]; then
rm -f /usr/local/hestia/nginx-system/etc/$PROXY_SYSTEM/$pconfd/domains/$WEBMAIL_ALIAS.$domain.ssl.conf
else
rm -f /etc/$PROXY_SYSTEM/$pconfd/domains/$WEBMAIL_ALIAS.$domain.ssl.conf
fi
# Remove SSL certificates # Remove SSL certificates
rm -f $HOMEDIR/$user/conf/mail/$domain/ssl/* rm -f $HOMEDIR/$user/conf/mail/$domain/ssl/*
@@ -1006,13 +1035,23 @@ add_webmail_config() {
if [[ "$2" =~ stpl$ ]]; then if [[ "$2" =~ stpl$ ]]; then
if [ -n "$WEB_SYSTEM" ]; then if [ -n "$WEB_SYSTEM" ]; then
forcessl="$HOMEDIR/$user/conf/mail/$domain/$WEB_SYSTEM.forcessl.conf" forcessl="$HOMEDIR/$user/conf/mail/$domain/$WEB_SYSTEM.forcessl.conf"
rm -f /etc/$1/$confd/domains/$WEBMAIL_ALIAS.$domain.ssl.conf if [ "$1" = "nginx" ]; then
ln -s $conf /etc/$1/$confd/domains/$WEBMAIL_ALIAS.$domain.ssl.conf rm -f /usr/local/hestia/nginx-system/etc/$1/$confd/domains/$WEBMAIL_ALIAS.$domain.ssl.conf
ln -s $conf /usr/local/hestia/nginx-system/etc/$1/$confd/domains/$WEBMAIL_ALIAS.$domain.ssl.conf
else
rm -f /etc/$1/$confd/domains/$WEBMAIL_ALIAS.$domain.ssl.conf
ln -s $conf /etc/$1/$confd/domains/$WEBMAIL_ALIAS.$domain.ssl.conf
fi
fi fi
if [ -n "$PROXY_SYSTEM" ]; then if [ -n "$PROXY_SYSTEM" ]; then
forcessl="$HOMEDIR/$user/conf/mail/$domain/$PROXY_SYSTEM.forcessl.conf" forcessl="$HOMEDIR/$user/conf/mail/$domain/$PROXY_SYSTEM.forcessl.conf"
rm -f /etc/$1/$confd/domains/$WEBMAIL_ALIAS.$domain.ssl.conf if [ "$1" = "nginx" ]; then
ln -s $conf /etc/$1/$confd/domains/$WEBMAIL_ALIAS.$domain.ssl.conf rm -f /usr/local/hestia/nginx-system/etc/$1/$confd/domains/$WEBMAIL_ALIAS.$domain.ssl.conf
ln -s $conf /usr/local/hestia/nginx-system/etc/$1/$confd/domains/$WEBMAIL_ALIAS.$domain.ssl.conf
else
rm -f /etc/$1/$confd/domains/$WEBMAIL_ALIAS.$domain.ssl.conf
ln -s $conf /etc/$1/$confd/domains/$WEBMAIL_ALIAS.$domain.ssl.conf
fi
fi fi
# Add rewrite rules to force HTTPS/SSL connections # Add rewrite rules to force HTTPS/SSL connections
@@ -1027,12 +1066,22 @@ add_webmail_config() {
find $HOMEDIR/$user/conf/mail/ -maxdepth 1 -type f \( -name "$domain.*" -o -name "ssl.$domain.*" -o -name "*nginx.$domain.*" \) -exec rm {} \; find $HOMEDIR/$user/conf/mail/ -maxdepth 1 -type f \( -name "$domain.*" -o -name "ssl.$domain.*" -o -name "*nginx.$domain.*" \) -exec rm {} \;
else else
if [ -n "$WEB_SYSTEM" ]; then if [ -n "$WEB_SYSTEM" ]; then
rm -f /etc/$1/$confd/domains/$WEBMAIL_ALIAS.$domain.conf if [ "$1" = "nginx" ]; then
ln -s $conf /etc/$1/$confd/domains/$WEBMAIL_ALIAS.$domain.conf rm -f /usr/local/hestia/nginx-system/etc/$1/$confd/domains/$WEBMAIL_ALIAS.$domain.conf
ln -s $conf /usr/local/hestia/nginx-system/etc/$1/$confd/domains/$WEBMAIL_ALIAS.$domain.conf
else
rm -f /etc/$1/$confd/domains/$WEBMAIL_ALIAS.$domain.conf
ln -s $conf /etc/$1/$confd/domains/$WEBMAIL_ALIAS.$domain.conf
fi
fi fi
if [ -n "$PROXY_SYSTEM" ]; then if [ -n "$PROXY_SYSTEM" ]; then
rm -f /etc/$1/$confd/domains/$WEBMAIL_ALIAS.$domain.conf if [ "$1" = "nginx" ]; then
ln -s $conf /etc/$1/$confd/domains/$WEBMAIL_ALIAS.$domain.conf rm -f /usr/local/hestia/nginx-system/etc/$1/$confd/domains/$WEBMAIL_ALIAS.$domain.conf
ln -s $conf /usr/local/hestia/nginx-system/etc/$1/$confd/domains/$WEBMAIL_ALIAS.$domain.conf
else
rm -f /etc/$1/$confd/domains/$WEBMAIL_ALIAS.$domain.conf
ln -s $conf /etc/$1/$confd/domains/$WEBMAIL_ALIAS.$domain.conf
fi
fi fi
# Clear old configurations # Clear old configurations
find $HOMEDIR/$user/conf/mail/ -maxdepth 1 -type f \( -name "$domain.*" \) -exec rm {} \; find $HOMEDIR/$user/conf/mail/ -maxdepth 1 -type f \( -name "$domain.*" \) -exec rm {} \;
@@ -1053,12 +1102,20 @@ del_webmail_config() {
fi fi
if [ -n "$WEB_SYSTEM" ]; then if [ -n "$WEB_SYSTEM" ]; then
rm -f $HOMEDIR/$user/$confd/mail/$domain/$WEB_SYSTEM.conf rm -f $HOMEDIR/$user/$confd/mail/$domain/$WEB_SYSTEM.conf
rm -f /etc/$WEB_SYSTEM/$confd/domains/$WEBMAIL_ALIAS.$domain.conf if [ "$WEB_SYSTEM" = "nginx" ]; then
rm -f /usr/local/hestia/nginx-system/etc/$WEB_SYSTEM/$confd/domains/$WEBMAIL_ALIAS.$domain.conf
else
rm -f /etc/$WEB_SYSTEM/$confd/domains/$WEBMAIL_ALIAS.$domain.conf
fi
fi fi
if [ -n "$PROXY_SYSTEM" ]; then if [ -n "$PROXY_SYSTEM" ]; then
rm -f $HOMEDIR/$user/conf/mail/$domain/$PROXY_SYSTEM.*conf rm -f $HOMEDIR/$user/conf/mail/$domain/$PROXY_SYSTEM.*conf
rm -f /etc/$PROXY_SYSTEM/$pconfd/domains/$WEBMAIL_ALIAS.$domain.conf if [ "$PROXY_SYSTEM" = "nginx" ]; then
rm -f /usr/local/hestia/nginx-system/etc/$PROXY_SYSTEM/$pconfd/domains/$WEBMAIL_ALIAS.$domain.conf
else
rm -f /etc/$PROXY_SYSTEM/$pconfd/domains/$WEBMAIL_ALIAS.$domain.conf
fi
fi fi
} }
@@ -1076,12 +1133,20 @@ del_webmail_ssl_config() {
fi fi
if [ -n "$WEB_SYSTEM" ]; then if [ -n "$WEB_SYSTEM" ]; then
rm -f $HOMEDIR/$user/conf/mail/$domain/$WEB_SYSTEM.*ssl.conf rm -f $HOMEDIR/$user/conf/mail/$domain/$WEB_SYSTEM.*ssl.conf
rm -f /etc/$WEB_SYSTEM/$confd/domains/$WEBMAIL_ALIAS.$domain.ssl.conf if [ "$WEB_SYSTEM" = "nginx" ]; then
rm -f /usr/local/hestia/nginx-system/etc/$WEB_SYSTEM/$confd/domains/$WEBMAIL_ALIAS.$domain.ssl.conf
else
rm -f /etc/$WEB_SYSTEM/$confd/domains/$WEBMAIL_ALIAS.$domain.ssl.conf
fi
fi fi
if [ -n "$PROXY_SYSTEM" ]; then if [ -n "$PROXY_SYSTEM" ]; then
rm -f $HOMEDIR/$user/conf/mail/$domain/$PROXY_SYSTEM.*ssl.conf rm -f $HOMEDIR/$user/conf/mail/$domain/$PROXY_SYSTEM.*ssl.conf
rm -f /etc/$PROXY_SYSTEM/$pconfd/domains/$WEBMAIL_ALIAS.$domain.ssl.conf if [ "$PROXY_SYSTEM" = "nginx" ]; then
rm -f /usr/local/hestia/nginx-system/etc/$PROXY_SYSTEM/$pconfd/domains/$WEBMAIL_ALIAS.$domain.ssl.conf
else
rm -f /etc/$PROXY_SYSTEM/$pconfd/domains/$WEBMAIL_ALIAS.$domain.ssl.conf
fi
fi fi
} }
@@ -1187,14 +1252,14 @@ is_base_domain_owner() {
#----------------------------------------------------------# #----------------------------------------------------------#
process_http2_directive() { process_http2_directive() {
if [ -e /etc/nginx/conf.d/http2-directive.conf ]; then if [ -e /usr/local/hestia/nginx-system/etc/nginx/conf.d/http2-directive.conf ]; then
while IFS= read -r old_param; do while IFS= read -r old_param; do
new_param="$(echo "$old_param" | sed 's/\shttp2//')" new_param="$(echo "$old_param" | sed 's/\shttp2//')"
sed -i "s/$old_param/$new_param/" "$1" sed -i "s/$old_param/$new_param/" "$1"
done < <(grep -E "listen.*(\bssl\b(\s|.+){1,}\bhttp2\b|\bhttp2\b(\s|.+){1,}\bssl\b).*;" "$1") done < <(grep -E "listen.*(\bssl\b(\s|.+){1,}\bhttp2\b|\bhttp2\b(\s|.+){1,}\bssl\b).*;" "$1")
else else
if version_ge "$(nginx -v 2>&1 | cut -d'/' -f2)" "1.25.1"; then if version_ge "$(/usr/local/hestia/nginx-system/sbin/nginx -v 2>&1 | cut -d'/' -f2)" "1.25.1"; then
echo "http2 on;" > /etc/nginx/conf.d/http2-directive.conf echo "http2 on;" > /usr/local/hestia/nginx-system/etc/nginx/conf.d/http2-directive.conf
while IFS= read -r old_param; do while IFS= read -r old_param; do
new_param="$(echo "$old_param" | sed 's/\shttp2//')" new_param="$(echo "$old_param" | sed 's/\shttp2//')"

View File

@@ -736,7 +736,7 @@ rebuild_mail_domain_conf() {
echo "$account@$domain_idn:$user_rate_limit" >> $HOMEDIR/$user/conf/mail/$domain/limits echo "$account@$domain_idn:$user_rate_limit" >> $HOMEDIR/$user/conf/mail/$domain/limits
else else
#revert to system value #revert to system value
system=$(cat /etc/exim4/limit.conf) system=$(cat /etc/exim/limit.conf)
sed -i "/^$account@$domain_idn:/ d" $HOMEDIR/$user/conf/mail/$domain/limits sed -i "/^$account@$domain_idn:/ d" $HOMEDIR/$user/conf/mail/$domain/limits
echo "$account@$domain_idn:$system" >> $HOMEDIR/$user/conf/mail/$domain/limits echo "$account@$domain_idn:$system" >> $HOMEDIR/$user/conf/mail/$domain/limits
fi fi

View File

@@ -536,7 +536,7 @@ upgrade_cloudflare_ip() {
cf_ips="$(curl -fsLm5 --retry 2 https://api.cloudflare.com/client/v4/ips)" cf_ips="$(curl -fsLm5 --retry 2 https://api.cloudflare.com/client/v4/ips)"
if [ -n "$cf_ips" ] && [ "$(echo "$cf_ips" | jq -r '.success//""')" = "true" ]; then if [ -n "$cf_ips" ] && [ "$(echo "$cf_ips" | jq -r '.success//""')" = "true" ]; then
cf_inc="/etc/nginx/conf.d/cloudflare.inc" cf_inc="/usr/local/hestia/nginx-system/etc/nginx/conf.d/cloudflare.inc"
echo "[ * ] Updating Cloudflare IP Ranges for NGINX..." echo "[ * ] Updating Cloudflare IP Ranges for NGINX..."
echo "# Cloudflare IP Ranges" > $cf_inc echo "# Cloudflare IP Ranges" > $cf_inc

View File

@@ -0,0 +1,630 @@
#!/usr/bin/env ruby
require 'json'
require 'net/http'
require 'uri'
require 'openssl'
class BunkerWebApiError < StandardError; end
class HestiaBunkerWebApi
# Override puts to accumulate logs into @extra_info
def puts(*args)
@extra_info ||= ""
@extra_info << args.join("\n") << "\n"
end
# Accessor for @extra_info
def extra_info
@extra_info || ""
end
end
# Hook to wrap methods of HestiaBunkerWebApi to reset @extra_info at start
class Module
alias_method :orig_method_added, :method_added
def method_added(name)
orig_method_added(name)
# Skip wrapping for the overridden puts method
return if name == :puts
if self.name == 'HestiaBunkerWebApi'
@__wrapping ||= false
return if @__wrapping
@__wrapping = true
original = instance_method(name)
define_method(name) do |*args, &block|
@extra_info = ""
original.bind(self).call(*args, &block)
end
@__wrapping = false
end
end
end
class HestiaBunkerWebApi
# Retrieve API username and password from /etc/bunkerweb/api.env if available
def get_api_user_password
env_path = "/etc/bunkerweb/api.env"
return nil unless File.file?(env_path)
username = nil
password = nil
File.foreach(env_path) do |line|
line.strip!
next if line.empty? || line.start_with?('#')
key, value = line.split('=', 2)
next unless key && value
case key
when 'API_USERNAME'
username = value
when 'API_PASSWORD'
password = value
end
end
if username && password
[username, password]
else
nil
end
end
def initialize(api_url, username = nil, password = nil)
@api_base = api_url
if username.nil?
result = get_api_user_password
if result.nil?
raise BunkerWebApiError.new("Authentication error: no username or password")
else
@username = result[0]
@password = result[1]
end
else
@username = username
@password = password
end
@token = nil
@extra_info = ""
# Authenticate and get token
authenticate!
puts "[INFO] Successfully authenticated with BunkerWeb API"
end
def authenticate!
uri = URI(@api_base)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = (uri.scheme == "https")
# Try both Basic Auth and JSON body with credentials
request = Net::HTTP::Post.new("/auth")
request.content_type = "application/json"
request.body = { username: @username, password: @password }.to_json
response = http.request(request)
if response.code != '200'
raise BunkerWebApiError.new("Authentication failed: #{response.code} - #{response.message}")
end
body = JSON.parse(response.body)
unless body['token']
raise BunkerWebApiError.new("Authentication succeeded but no token received")
end
@token = body['token']
rescue => e
raise BunkerWebApiError.new("Authentication error: #{e.message}")
end
def api_call(method, path, headers = {}, body = nil)
uri = URI(@api_base + path)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = (uri.scheme == "https")
request = case method
when "GET"
Net::HTTP::Get.new(uri.path)
when "POST"
req = Net::HTTP::Post.new(uri.path)
req.body = body if body
req
when "PATCH"
req = Net::HTTP::Patch.new(uri.path)
req.body = body if body
req
when "DELETE"
Net::HTTP::Delete.new(uri.path)
else
raise BunkerWebApiError.new("Unsupported HTTP method: #{method}")
end
# Add Authorization header with token for most operations (except /auth)
unless path == "/auth"
headers["Authorization"] = "Bearer #{@token}" if @token
end
# Add Content-Type if not already set and we have a body
if body && !headers.key?("Content-Type")
headers["Content-Type"] = "application/json"
end
headers.each { |k, v| request[k] = v }
response = http.request(request)
parsed_body = begin
JSON.parse(response.body)
rescue => e
nil
end
{ status: response.code.to_i, body: parsed_body || {}, raw_body: response.body }
rescue => e
raise BunkerWebApiError.new("API call to #{path} failed: #{e.message}")
end
# === Service Operations ===
def create_service(service_name, options = {})
@extra_info = ""
# Create a new service with the given configuration.
#
# Args:
# service_name (String): The domain name for this service (server_name)
# options (Hash): Service configuration including:
# - USE_TEMPLATE (default: "high")
# - USE_SSL (default: "no") - if not "no", you need CERTIFICATE and KEY paths
# - REVERSE_PROXY_HOST (optional)
# - REVERSE_PROXY_URL (optional, default: "~ ^/(.*)$")
# - Additional options like:
# - USE_REVERSE_PROXY (default: "yes" if REVERSE_PROXY_HOST is set)
# - USE_REAL_IP, REAL_IP_FROM
# - USE_MODSECURITY, USE_ANTIBOT
# - LISTEN_HTTP_PORT, LISTEN_HTTPS_PORT
# - CERTIFICATE_FILE_PATH (if USE_SSL != "no")
# - KEY_FILE_PATH (if USE_SSL != "no")
# Returns: Hash with creation response
variables = {
"USE_TEMPLATE" => options[:use_template] || "high",
"USE_REVERSE_PROXY" => options[:reverse_proxy_host].nil? ? "no" : "yes",
"LIMIT_REQ_RATE" => options[:limit_req_rate] || "10r/s",
}
# SSL configuration - only if USE_SSL != "no"
ssl_enabled = options[:ssl] && options[:ssl] != "no"
variables["USE_CUSTOM_SSL"] = ssl_enabled ? "yes" : "no"
if ssl_enabled
unless options[:certificate_path] && options[:key_path]
raise BunkerWebApiError.new("Certificate and Key paths are required when USE_SSL is enabled")
end
# Set certificate paths in variables
variables["CUSTOM_SSL_CERT"] = options[:certificate_path]
variables["CUSTOM_SSL_KEY"] = options[:key_path]
variables["LISTEN_HTTPS_PORT"] = (options[:https_port] || "443").to_s
variables["USE_REVERSE_PROXY_SSL"] = options[:reverse_proxy_ssl] || "yes"
else
# No SSL - HTTP only
# API expects string "null", not nil/JSON null
variables["LISTEN_HTTPS_PORT"] = "null"
variables["LISTEN_HTTP_PORT"] = (options[:http_port] || "80").to_s
end
# Reverse proxy configuration if specified
if options[:reverse_proxy_host]
variables["REVERSE_PROXY_HOST"] = options[:reverse_proxy_host]
variables["REVERSE_PROXY_URL"] = options[:reverse_proxy_url] || "~ ^(?!/challenge)(.*)$"
# Real IP settings for reverse proxy
unless options[:real_ip_from].nil?
variables["USE_REAL_IP"] = "yes"
variables["REAL_IP_FROM"] = options[:real_ip_from]
end
# Additional security settings from High template
variables["USE_MODSECURITY"] = options[:use_modsecurity] || "yes"
variables["USE_ANTIBOT"] = options[:anti_bot] || "captcha"
end
variables["ANTIBOT_IGNORE_URI"] = options[:anti_bot_ignore_uri] || "^/\.well-known/acme-challenge/.+$"
variables["LETS_ENCRYPT_PASSTHROUGH"] = options[:lets_encrypt_passthrough] || "yes"
service_body = {
server_name: service_name,
is_draft: false,
variables: variables
}
response = api_call("POST", "/services", {}, JSON.generate(service_body))
# Accept both 201 (Created) and 200 (OK) for successful creation
if [201, 200].include?(response[:status])
puts "[INFO] Service '#{service_name}' created successfully"
elsif response[:status] == 409
raise BunkerWebApiError.new("Service '#{service_name}' already exists")
else
raise BunkerWebApiError.new("Failed to create service: status=#{response[:status]}, body=#{response[:raw_body]}")
end
return response[:body] || {}
end
def update_service_ssl(service_name, certificate_path, key_path, https_port = nil)
# Update or change the SSL certificate path for an existing service.
#
# Args:
# service_name (String): Name of the service to update
# certificate_path (String): Path to the SSL certificate file
# key_path (String): Path to the SSL private key file
# https_port (Integer, optional): HTTPS port (default 443)
# Returns: Hash with update response
@extra_info = ""
# First get current service configuration to preserve existing settings
get_service_response = api_call("GET", "/services/#{service_name}", {})
if get_service_response[:status] != 200
raise BunkerWebApiError.new("Service '#{service_name}' not found")
end
# Extract variables from config - API stores them in 'config' not 'variables'
# Each variable is a hash with 'value' as the actual setting value
current_vars = {}
if get_service_response[:body]["config"]
get_service_response[:body]["config"].each do |key, value_hash|
if value_hash.is_a?(Hash) && value_hash.key?("value")
current_vars[key] = value_hash["value"]
else
current_vars[key] = value_hash
end
end
end
# Update SSL settings
updated_vars = {
"USE_CUSTOM_SSL" => "yes",
"CUSTOM_SSL_CERT" => certificate_path,
"CUSTOM_SSL_KEY" => key_path,
"LISTEN_HTTPS_PORT" => (https_port || "443").to_s,
"USE_REVERSE_PROXY_SSL" => "yes"
}
# Merge with existing variables (keep non-SSL settings)
final_vars = current_vars.merge(updated_vars)
service_body = {
server_name: nil, # Not changing name
is_draft: false, # Keep as online
variables: final_vars
}
response = api_call("PATCH", "/services/#{service_name}", {}, JSON.generate(service_body))
if response[:status] == 200
puts "[INFO] SSL configuration updated for service '#{service_name}'"
else
raise BunkerWebApiError.new("Failed to update SSL configuration: status=#{response[:status]}, body=#{response[:raw_body]}")
end
return response[:body] || {}
end
def set_alias(service_name, list_aliases)
@extra_info = ""
# First get current service configuration to preserve existing settings
get_service_response = api_call("GET", "/services/#{service_name}", {})
if get_service_response[:status] != 200
raise BunkerWebApiError.new("Service '#{service_name}' not found")
end
# Save the entire current configuration (except server_name which will be replaced)
current_config = get_service_response[:body]
# Extract variables from config - API stores them in 'config' not 'variables'
# Each variable is a hash with 'value' as the actual setting value
current_vars = {}
if current_config["config"]
current_config["config"].each do |key, value_hash|
if key != "SERVER_NAME"
if value_hash.is_a?(Hash) && value_hash.key?("value")
current_vars[key] = value_hash["value"]
else
current_vars[key] = value_hash
end
end
end
end
# Clean the list_aliases string according to the rules
cleaned_aliases = list_aliases.to_s
# Replace commas (with or without space) with a single space
cleaned_aliases.gsub!(/,\s?/, ' ')
# Replace multiple spaces with a single space
cleaned_aliases.gsub!(/\s{2,}/, ' ')
# Strip leading/trailing whitespace
cleaned_aliases.strip!
# Ensure the main service name appears first in the alias list
aliases_array = cleaned_aliases.split(' ')
if aliases_array.include?(service_name)
aliases_array.delete(service_name)
end
# Rebuild cleaned string
cleaned_aliases = aliases_array.join(' ')
current_vars["SERVER_NAME"]=cleaned_aliases
# Step 1: Delete the old service
delete_response = api_call("DELETE", "/services/#{service_name}")
if delete_response[:status] != 200 && delete_response[:status] != 204
raise BunkerWebApiError.new("Failed to delete service '#{service_name}': status=#{delete_response[:status]}")
end
puts "[INFO] Service '#{service_name}' deleted"
# Step 2: Create a new service with the cleaned alias list as server_name
# and preserve all existing configuration variables
service_body = {
server_name: service_name, # Use the full alias list including service_name first
is_draft: current_config["is_draft"] || false,
variables: current_vars # Preserve all existing variables from the original service
}
post_response = api_call("POST", "/services", {}, JSON.generate(service_body))
if post_response[:status] == 200 || post_response[:status] == 201
puts "[INFO] Service recreated successfully with aliases: #{cleaned_aliases}"
elsif post_response[:status] == 409
raise BunkerWebApiError.new("Service '#{service_name}' already exists")
else
raise BunkerWebApiError.new("Failed to create service: status=#{post_response[:status]}, body=#{post_response[:raw_body]}")
end
return post_response || {}
end
def delete_service_ssl(service_name)
@extra_info = ""
# First get current service configuration to preserve existing settings
get_service_response = api_call("GET", "/services/#{service_name}", {})
if get_service_response[:status] != 200
raise BunkerWebApiError.new("Service '#{service_name}' not found")
end
# Extract variables from config - API stores them in 'config' not 'variables'
# Each variable is a hash with 'value' as the actual setting value
current_vars = {}
if get_service_response[:body]["config"]
get_service_response[:body]["config"].each do |key, value_hash|
if value_hash.is_a?(Hash) && value_hash.key?("value")
current_vars[key] = value_hash["value"]
else
current_vars[key] = value_hash
end
end
end
# Update SSL settings
updated_vars = {
"USE_CUSTOM_SSL" => "no",
"CUSTOM_SSL_CERT" => "",
"CUSTOM_SSL_KEY" => ""
}
# Merge with existing variables (keep non-SSL settings)
final_vars = current_vars.merge(updated_vars)
service_body = {
server_name: nil, # Not changing name
is_draft: false, # Keep as online
variables: final_vars
}
response = api_call("PATCH", "/services/#{service_name}", {}, JSON.generate(service_body))
if response[:status] == 200
puts "[INFO] SSL configuration updated for service '#{service_name}'"
else
raise BunkerWebApiError.new("Failed to update SSL configuration: status=#{response[:status]}, body=#{response[:raw_body]}")
end
return response[:body] || {}
end
def delete_service(service_name)
# Delete a service by its name.
#
# Args:
# service_name (String): Name of the service to delete
# Returns: Hash with deletion response
@extra_info = ""
# Verify service exists first
get_response = api_call("GET", "/services/#{service_name}", {})
if get_response[:status] != 200
raise BunkerWebApiError.new("Service '#{service_name}' not found")
end
response = api_call("DELETE", "/services/#{service_name}")
if response[:status] == 200 || response[:status] == 204
puts "[INFO] Service '#{service_name}' deleted successfully"
return response[:body] || {}
else
raise BunkerWebApiError.new("Failed to delete service: status=#{response[:status]}, body=#{response[:raw_body]}")
end
end
# === Additional Utility Methods ===
def list_services(drafts = false)
# List all services.
#
# Args:
# drafts (Boolean): Include draft services (default: false, set to true to include drafts)
# Returns: Array of service objects
@extra_info = ""
response = api_call("GET", "/services")
if response[:status] != 200
raise BunkerWebApiError.new("Failed to list services: status=#{response[:status]}")
end
return response[:body] || []
end
def get_service(service_name)
# Get details of a specific service.
#
# Args:
# service_name (String): Name of the service to retrieve
# Returns: Hash with service configuration
@extra_info = ""
response = api_call("GET", "/services/#{service_name}")
if response[:status] != 200
raise BunkerWebApiError.new("Service '#{service_name}' not found")
end
return response[:body] || {}
end
def reload_instance(instance_hostname = nil)
# Reload configuration on an instance.
#
# Args:
# instance_hostname (String, optional): Instance hostname to reload (if nil, reloads all instances)
# Returns: Hash with reload response
@extra_info = ""
path = if instance_hostname.nil?
"/instances/reload"
else
"/instances/#{instance_hostname}/reload"
end
response = api_call("POST", "#{path}?test=no")
if response[:status] == 200 || response[:status] == 201
puts "[INFO] Configuration reloaded successfully"
return response[:body] || {}
else
raise BunkerWebApiError.new("Failed to reload configuration: status=#{response[:status]}")
end
end
def list_instances()
# List all registered instances.
# Returns: Array of instance objects
@extra_info = ""
response = api_call("GET", "/instances")
if response[:status] != 200
raise BunkerWebApiError.new("Failed to list instances: status=#{response[:status]}")
end
return response[:body] || []
end
def create_instance(hostname, name = nil, port = 8888, https_port = nil)
# Create/register a new BunkerWeb instance (worker node).
#
# Args:
# hostname (String): IP address or hostname of the worker node
# name (String, optional): Human-readable name for the instance
# port (Integer): API port on the worker node (default 8888)
# https_port (Integer, optional): HTTPS port
# Returns: Hash with creation response
@extra_info = ""
instance_body = {
hostname: hostname,
name: name || "BunkerWeb Instance",
port: port,
listen_https: !https_port.nil?,
https_port: https_port,
server_name: hostname,
method: "api" # Using API deployment method
}
response = api_call("POST", "/instances", {}, JSON.generate(instance_body))
if response[:status] == 201
puts "[INFO] Instance '#{hostname}' registered successfully"
elsif response[:status] == 409
# Instance already exists - that's OK, we just want to use it
puts "[INFO] Instance '#{hostname}' already exists, will be used for this service"
else
raise BunkerWebApiError.new("Failed to create instance: status=#{response[:status]}, body=#{response[:raw_body]}")
end
return response[:body] || {}
end
def delete_instance(hostname)
"""
Delete a registered instance.
Args:
hostname (String): Hostname of the instance to delete
"""
@extra_info = ""
response = api_call("DELETE", "/instances/#{hostname}")
if response[:status] == 200 || response[:status] == 204
puts "[INFO] Instance '#{hostname}' deleted successfully"
return true
else
raise BunkerWebApiError.new("Failed to delete instance: status=#{response[:status]}")
end
end
end
# === Example Usage (can be run as script) ===
if __FILE__ == $0
# Example usage demonstration
begin
api = HestiaBunkerWebApi.new(
"http://127.0.0.1:8888",
"admin",
"your_password"
)
puts ""
puts "[INFO] Creating service 'example.my.domain'"
result = api.create_service("example.my.domain", {
reverse_proxy_host: "http://192.168.3.51:8078",
ssl: "no",
use_template: "high"
})
puts ""
puts "[INFO] Listing services:"
services = api.list_services()
services.each { |s| puts "- #{s['server_name']}" }
rescue BunkerWebApiError => e
puts "[ERROR] #{e.message}"
exit 1
end
end

View File

@@ -0,0 +1,467 @@
# HestiaBunkerWebApi - Ruby класс для работы с BunkerWeb API
## Описание
Класс `HestiaBunkerWebApi` предоставляет простой интерфейс для управления сервисами BunkerWeb через REST API. Класс реализует:
- **Аутентификацию** с получением токена
- **Управление сервисами** (создание, обновление, удаление)
- **Управление SSL сертификатами**
- **Управление instances** (worker nodes)
- **Полное исключение ошибок** при любых проблемах
## Установка и импорт
```bash
# Ruby 3.3+ рекомендуется
ruby --version
# ruby 3.3.x or later
# Класс использует стандартные библиотеки Ruby:
# - json (для JSON парсинга)
# - net/http (для HTTP запросов)
# - uri (для URL парсинга)
```
## Использование класса
### Базовое использование
```ruby
require_relative "HestiaBunkerWebApi.rb"
# Создаём экземпляр API с аутентификацией
api = HestiaBunkerWebApi.new(
"http://127.0.0.1:8888", # URL API (можно https://)
"admin", # username
"password" # password
)
# или
api = HestiaBunkerWebApi.new(
"http://127.0.0.1:8888", # URL API (можно https://)
)
# в этом случае пароль и логин читаются автоматически из файла /etc/bunkerweb/api.env
# При создании экземпляра автоматически происходит аутентификация
# Если ошибка - выбрасывается BunkerWebApiError с описанием проблемы
```
### Обработка ошибок
Все ошибки наследуются от `StandardError` через класс `BunkerWebApiError`:
```ruby
begin
api.create_service("example.domain", options)
rescue BunkerWebApiError => e
puts "[ERROR] Ошибка API: #{e.message}"
# Примеры возможных ошибок:
# - "Authentication failed: 401 - Unauthorized"
# - "Service 'x' already exists"
# - "Certificate and Key paths are required when USE_SSL is enabled"
# - "Failed to create service: status=500, body={...}"
exit 1
end
```
## Методы класса
### Конструктор
```ruby
HestiaBunkerWebApi.new(api_url, username, password)
```
**Параметры:**
- `api_url` - URL BunkerWeb API в формате `http://ip:port` или `https://ip:port`
- `username` - имя администратора для аутентификации
- `password` - пароль для аутентификации
**Действие:** При создании автоматически пытается аутентифицироваться через POST /auth и сохраняет токен.
### Создание сервиса
```ruby
api.create_service(service_name, options = {})
```
**Параметры:**
- `service_name` - имя домена/сервиса (например, "example.my.domain")
- `options` - хэш с конфигурацией:
| Параметр | Тип | Описание | Пример |
|----------|-----|----------|--------|
| `ssl` | String | "yes" для SSL, "no" для HTTP | `"no"` |
| `certificate_path` | String | Путь к SSL сертификату (если ssl="yes") | `"/etc/ssl/certs/example.crt"` |
| `key_path` | String | Путь к приватному ключу (если ssl="yes") | `"/etc/ssl/private/example.key"` |
| `use_template` | String | Безопасность шаблона | `"high"` (default) |
| `reverse_proxy_host` | String | Target reverse proxy | `"http://192.168.3.51:8078"` |
| `reverse_proxy_url` | String | URL трансформация | `"~ ^/(.*)$"` |
| `real_ip_from` | String | CIDR trusted network для RealIP | `"192.168.3.0/24"` |
| `use_modsecurity` | String | WAF включение | `"yes"` (default) |
| `anti_bot` | String | Bot protection | `"captcha"` (default) |
| `http_port` | Integer/nil | HTTP порт | `"80"` или `null` |
| `https_port` | Integer | HTTPS порт | `443` или `null` |
**Пример - создание reverse proxy сервиса без SSL:**
```ruby
result = api.create_service("example.my.domain", {
ssl: "no",
reverse_proxy_host: "http://192.168.3.51:8078"
})
# Генерирует variables:
# - USE_TEMPLATE: "high"
# - USE_SSL: "no"
# - LISTEN_HTTPS_PORT: "null"
# - LISTEN_HTTP_PORT: "80"
# - USE_REVERSE_PROXY: "yes"
# - REVERSE_PROXY_HOST: "http://192.168.3.51:8078"
```
**Пример - создание сервиса с SSL + reverse proxy:**
```ruby
result = api.create_service("example.my.domain", {
ssl: "yes", # Включаем SSL
certificate_path: "/etc/ssl/certs/example.crt", # Путь к сертификату
key_path: "/etc/ssl/private/example.key", # Путь к ключу
reverse_proxy_host: "http://192.168.3.51:8078", # Reverse proxy target
use_template: "high",
anti_bot: "captcha"
})
# Генерирует variables:
# - USE_TEMPLATE: "high"
# - USE_SSL: "yes"
# - SSL_CERTIFICATE_FILE_PATH: "/etc/ssl/certs/example.crt"
# - SSL_KEY_FILE_PATH: "/etc/ssl/private/example.key"
# - LISTEN_HTTPS_PORT: "443"
# - LISTEN_HTTP_PORT: "80"
```
### Обновление SSL сертификата для существующего сервиса
```ruby
api.update_service_ssl(service_name, certificate_path, key_path, https_port = nil)
```
**Параметры:**
- `service_name` - имя уже созданного сервиса
- `certificate_path` - новый путь к SSL сертификату
- `key_path` - новый путь к приватному ключу
- `https_port` (optional) - HTTPS порт (default: 443)
**Пример:**
```ruby
api.update_service_ssl(
"example.my.domain",
"/etc/ssl/certs/example.crt",
"/etc/ssl/private/example.key"
)
# Обновляет существующий сервис, сохраняя reverse proxy настройки
```
### Удаление сервиса
```ruby
api.delete_service(service_name)
```
**Параметры:**
- `service_name` - имя сервиса для удаления
**Пример:**
```ruby
api.delete_service("example.my.domain")
# Удаляет сервис и конфигурацию
```
### Получение списка всех сервисов
```ruby
api.list_services(drafts = false)
```
**Параметры:**
- `drafts` (optional) - включать draft сервисы (default: false)
**Возвращает:** Array of service objects
**Пример:**
```ruby
services = api.list_services()
services.each { |s| puts "- #{s['server_name']}" }
```
### Получение деталей конкретного сервиса
```ruby
api.get_service(service_name)
```
**Параметры:**
- `service_name` - имя сервиса для получения деталей
**Возвращает:** Hash with service configuration (variables, settings, etc.)
**Пример:**
```ruby
config = api.get_service("example.my.domain")
puts config.inspect
```
### Перезагрузка конфигурации на instance
```ruby
api.reload_instance(instance_hostname = nil)
```
**Параметры:**
- `instance_hostname` (optional) - hostname instance для перезагрузки (если nil, reloads all instances)
**Пример:**
```ruby
# Reload все instances
api.reload_instance()
# Reload конкретный instance
api.reload_instance("192.168.3.50")
```
### Получение списка всех instances
```ruby
api.list_instances()
```
**Возвращает:** Array of instance objects (hostname, name, port, etc.)
### Создание/регистрация BunkerWeb instance (worker node)
```ruby
api.create_instance(hostname, name = nil, port = 8888, https_port = nil)
```
**Параметры:**
- `hostname` - IP address или hostname worker node
- `name` (optional) - Human-readable имя instance
- `port` - API port на worker node (default: 8888)
- `https_port` (optional) - HTTPS port если есть
**Пример:**
```ruby
api.create_instance(
"192.168.3.50", # IP worker node
"BunkerWeb Worker Node", # Optional name
8888 # API port
)
# Возвращает: { status: 201/409, body: {...} }
# Если статус 201 - instance создан
# Если статус 409 - instance уже существует (это OK)
```
### Удаление BunkerWeb instance
```ruby
api.delete_instance(hostname)
```
**Параметры:**
- `hostname` - hostname instance для удаления
## Примеры полного использования
### Пример 1: Создание и управление сервисом
```ruby
require_relative "HestiaBunkerWebApi.rb"
begin
# 1. Подключаемся к API
api = HestiaBunkerWebApi.new(
"http://127.0.0.1:8888",
"admin",
"your_password"
)
# 2. Создаём reverse proxy сервис без SSL
result = api.create_service("u4.my.brp", {
ssl: "no",
reverse_proxy_host: "http://192.168.3.51:8078"
})
puts "[INFO] Service created: #{result.inspect}"
# 3. Добавляем SSL сертификат позже (если нужно)
api.update_service_ssl(
"u4.my.brp",
"/etc/ssl/certs/u4.crt",
"/etc/ssl/private/u4.key"
)
# 4. Проверяем список сервисов
services = api.list_services()
puts "[INFO] All services:"
services.each { |s| puts "- #{s['server_name']}" }
# 5. Удаление сервиса (при необходимости)
api.delete_service("u4.my.brp")
rescue BunkerWebApiError => e
puts "[ERROR] Ошибка API: #{e.message}"
exit 1
end
```
### Пример 2: Управление несколькими сервисами
```ruby
require_relative "HestiaBunkerWebApi.rb"
api = HestiaBunkerWebApi.new("http://127.0.0.1:8888", "admin", "password")
# Создаём несколько сервисов с разными конфигурациями
services_to_create = [
{ name: "service1.domain", ssl: "no", reverse_proxy_host: "http://192.168.3.50:80" },
{ name: "service2.domain", ssl: "yes", certificate_path: "/certs/service2.crt", key_path: "/keys/service2.key", reverse_proxy_host: "http://192.168.3.51:8078" }
]
services_to_create.each do |opts|
begin
api.create_service(opts[:name], opts)
rescue BunkerWebApiError => e
puts "[ERROR] #{e.message}" if e.message.include?("already exists")
end
end
# Reload конфигурации на instance
api.reload_instance("192.168.3.50")
```
### Пример 3: Обработка ошибок и логирование
```ruby
require_relative "HestiaBunkerWebApi.rb"
def safe_create_service(api_url, username, password, service_name, options)
begin
api = HestiaBunkerWebApi.new(api_url, username, password)
result = api.create_service(service_name, options)
return { success: true, data: result }
rescue BunkerWebApiError => e
if e.message.include?("Authentication")
puts "[FATAL] Authentication failed: #{e.message}"
elsif e.message.include?("already exists")
begin
existing = api.get_service(service_name)
return { success: false, already_exists: true, service: existing }
rescue => get_error
return { success: false, error: "Can't retrieve service: #{get_error.message}" }
else
return { success: false, error: e.message }
end
end
{ success: false, error: "Unknown error" }
end
# Использование
result = safe_create_service("http://127.0.0.1:8888", "admin", "password", "example.domain", { ssl: "yes" })
if result[:success]
puts "[SUCCESS] Service created"
elsif result[:already_exists]
puts "[INFO] Service exists:"
puts JSON.generate(result[:service])
else
puts "[FAILED] #{result[:error]}"
end
```
## Ошибки и их обработка
### Типичные ошибки:
| Код ответа | Описание | Пример сообщения |
|------------|----------|------------------|
| **401** | Authentication failed | "Authentication failed: 401 - Unauthorized" |
| **200 (no token)** | Auth passed but no token | "Authentication succeeded but no token received" |
| **Connection error** | API недоступен | "Authentication error: Connection refused" |
| **409 Conflict** | Service already exists | "Service 'x' already exists" |
| **422 Unprocessable Entity** | Invalid data (например, LISTEN_HTTPS_PORT = nil) | "Failed to create service: status=422..." |
| **429 Too Many Requests** | Rate limit exceeded | "Rate limit exceeded: 10 per 1 minute" |
### Решение проблем с rate limiting
Если получаете ошибку `429` (rate limit), нужно отключить или увеличить лимит в `/etc/bunkerweb/api.env`:
```bash
# Откройте конфиг и найдите секцию Rate limiting
nano /etc/bunkerweb/api.env
# Добавьте/измените:
API_RATE_LIMIT_ENABLED=no # Отключение rate limiting
# или
API_RATE_LIMIT=1000/minute # Увеличение лимита до 1000/m
```
Затем перезагрузите API service:
```bash
systemctl reload bunkerweb-api.service
```
## Особенности реализации
### 1. SSL сертификатные пути
Когда `ssl: "no"` - API ожидает `"LISTEN_HTTPS_PORT" => "null"` (строка), а не JSON null (`nil`):
```ruby
# ❌ Ошибка:
variables["LISTEN_HTTPS_PORT"] = nil # → 422 error
# ✅ Правильно:
variables["LISTEN_HTTPS_PORT"] = "null" # → 200 OK
```
### 2. Статусы ответа для создания сервиса
BunkerWeb API возвращает **200 OK** вместо стандартного **201 Created**:
```ruby
# Класс принимает оба статуса как успех:
if [201, 200].include?(response[:status])
puts "[INFO] Service created successfully"
end
```
### 3. Поддержка HTTP методов
Класс поддерживает все основные HTTP методы для API операций:
- **GET** - получение данных (services, instances)
- **POST** - создание (services, instances, auth)
- **PATCH** - обновление (services)
- **DELETE** - удаление (services, instances)
## Совместимость
- **Ruby**: 3.0+
- **BunkerWeb API**: 1.6.x и выше
- **ZooKeeper/Redis**: не требуются для этого класса (работает через HTTP API напрямую)
## Дополнительные ресурсы
- [Документация BunkerWeb API](https://docs.bunkerweb.io/api.md)
- [API Swagger docs at /docs](http://127.0.0.1:8888/docs)
- [OpenAPI schema](http://127.0.0.1:8888/openapi.json)

View File

@@ -0,0 +1,240 @@
#!/opt/brepo/ruby33/bin/ruby
require 'shellwords'
class BunkerwebWorker < Kernel::ModuleCoreWorker
MODULE_ID = "bunkerweb_module"
def info
{
ID: 5,
NAME: MODULE_ID,
DESCR: "Bunkerweb enabling",
REQ: "",
CONF: "yes",
}
end
def enable
log_file = get_log
f_inst_pp = get_module_paydata("bunkerweb_installer.yml")
if !check
inf = info
log("Req error, needed #{inf[:REQ]}")
"Req error, needed #{inf[:REQ]}"
else
begin
log("install packages for bunkerweb support: /usr/bin/ansible-playbook -vv #{f_inst_pp}")
result_action = `LC_ALL=C.UTF-8 /usr/bin/ansible-playbook -vv "#{f_inst_pp}" 2>&1`
ex_status = $?.exitstatus
if ex_status.to_i == 0 || ex_status.to_i == 2
log(result_action)
super
end
rescue => e
log("module installation error #{e.message} #{e.backtrace.first}")
"module installation error. See log #{log_file}"
end
end
end
def command(args)
return log_return("Not enough arguments. Needed command") if args.length < 1
log_file = get_log
m_command = args[0].strip
case m_command
when "add"
m_domain = args[1].strip unless args[1].nil?
m_ip = args[2].strip unless args[2].nil?
if m_domain.nil? || m_ip.nil?
log_return("Domain and IP should be specified. #{args}")
else
log("add domain to bunkerweb protection")
output = `/usr/local/hestia/bin/v-bunkerweb-module add #{m_domain} #{m_ip} shell`
exit_status = $?.exitstatus
if exit_status != 0
log_return("Command failed with status #{exit_status}")
else
ACTION_OK
end
end
when "delete"
m_domain = args[1].strip unless args[1].nil?
if m_domain.nil?
log_return("Domain should be specified. #{args}")
else
log("add domain to bunkerweb protection")
output = `/usr/local/hestia/bin/v-bunkerweb-module delete #{m_domain} shell`
exit_status = $?.exitstatus
if exit_status != 0
log_return("Command failed with status #{exit_status}")
else
ACTION_OK
end
end
when "addssl"
m_domain = args[1].strip unless args[1].nil?
m_ssl_cert = args[2].strip unless args[2].nil?
m_ssl_key = args[3].strip unless args[3].nil?
if m_domain.nil? || m_ssl_cert.nil? || m_ssl_key.nil? || m_ssl_cert.empty? || m_ssl_key.empty?
log_return("Domain, SSL cert and SSL key must be specified. #{args}")
else
log("add ssl cert to bunkerweb protection")
output = `/usr/local/hestia/bin/v-bunkerweb-module addssl #{m_domain} #{m_ssl_cert} #{m_ssl_key} shell`
exit_status = $?.exitstatus
if exit_status != 0
log_return("Command failed with status #{exit_status}")
else
ACTION_OK
end
end
when "updssl"
m_domain = args[1].strip unless args[1].nil?
m_ssl_cert = args[2].strip unless args[2].nil?
m_ssl_key = args[3].strip unless args[3].nil?
if m_domain.nil? || m_ssl_cert.nil? || m_ssl_key.nil? || m_ssl_cert.empty? || m_ssl_key.empty?
log_return("Domain, SSL cert and SSL key must be specified. #{args}")
else
log("update ssl cert to bunkerweb protection")
output = `/usr/local/hestia/bin/v-bunkerweb-module updssl #{m_domain} #{m_ssl_cert} #{m_ssl_key} shell`
exit_status = $?.exitstatus
if exit_status != 0
log_return("Command failed with status #{exit_status}")
else
ACTION_OK
end
end
when "deletessl"
m_domain = args[1].strip unless args[1].nil?
if m_domain.nil?
log_return("Domain should be specified. #{args}")
else
log("delete ssl cert to bunkerweb protection")
output = `/usr/local/hestia/bin/v-bunkerweb-module deletessl #{m_domain} shell`
exit_status = $?.exitstatus
if exit_status != 0
log_return("Command failed with status #{exit_status}")
else
ACTION_OK
end
end
when "list"
format = (args[1].nil? ? "shell" : args[1].strip)
log("list of services")
output = `/usr/local/hestia/bin/v-bunkerweb-module list #{format}`
exit_status = $?.exitstatus
if exit_status != 0
log_return("Command failed with status #{exit_status}")
else
puts output
ACTION_OK
end
when "passwd"
format = (args[1].nil? ? "shell" : args[1].strip)
cred = {}
api_file = "/etc/bunkerweb/api.env"
if File.exist?(api_file)
File.readlines(api_file).each do |line|
line.strip!
next if line.empty? || line.start_with?('#')
key, value = line.split('=', 2)
if %w[API_USERNAME API_PASSWORD].include?(key)
cred[key] = value
end
end
else
cred["API_USERNAME"] = nil
cred["API_PASSWORD"] = nil
end
cred["API_USERNAME"] ||= nil
cred["API_PASSWORD"] ||= nil
ui_file = "/etc/bunkerweb/ui.env"
if File.exist?(ui_file)
File.readlines(ui_file).each do |line|
line.strip!
next if line.empty? || line.start_with?('#')
key, value = line.split('=', 2)
if %w[ADMIN_USERNAME ADMIN_PASSWORD].include?(key)
cred[key] = value
end
end
else
cred["ADMIN_USERNAME"] = nil
cred["ADMIN_PASSWORD"] = nil
end
cred["ADMIN_USERNAME"] ||= nil
cred["ADMIN_PASSWORD"] ||= nil
result = []
result << cred
hestia_print_array_of_hashes(result, format, "API_USERNAME,API_PASSWORD,ADMIN_USERNAME,ADMIN_PASSWORD")
ACTION_OK
when "configure"
param1 = args[1]
param2 = args[2]
if param1 && param2 && !param1.strip.empty? && !param2.strip.empty?
cmd = "/usr/local/hestia/bin/v-bunkerweb-module-install #{Shellwords.escape(param1)} #{Shellwords.escape(param2)}"
else
cmd = "/usr/local/hestia/bin/v-bunkerweb-module-install"
end
output = `#{cmd} 2>&1`
exit_status = $?.exitstatus
if exit_status != 0
log_return("#{output}\nCommand failed with status #{exit_status}")
else
puts output
ACTION_OK
end
when "alias"
m_domain = args[1].strip unless args[1].nil?
m_alias = args[2].strip unless args[2].nil?
if m_domain.nil?
log_return("Domain should be specified. #{args}")
else
log("add alias #{m_alias} to domain #{m_domain} to bunkerweb protection")
output = `/usr/local/hestia/bin/v-bunkerweb-module alias #{m_domain} "#{m_alias}" shell`
exit_status = $?.exitstatus
if exit_status != 0
log_return("Command failed with status #{exit_status}")
else
ACTION_OK
end
end
when "help"
puts "#{$0} bunkerweb_module COMMAND [OPTIONS] [json|csv|plain]"
puts "COMMANDS:"
puts " add domain - add domain to bunkerweb"
puts " delete domain - delete domain from bunkerweb"
puts " addssl domain [path_to_cert] [path_to_key] - add existsing certificate to bunkerweb domain"
puts " updssl domain [path_to_cert] [path_to_key] - update existsing certificate to bunkerweb domain"
puts " passwd - get ui and api passwd"
puts " configure [path_to_cert] [path_to_key] - start initial setup of bunkerweb should do only once"
puts " help - help"
ACTION_OK
else
log_return("Unknown command. #{args}")
end
end
implements IPluginInterface
end
module BunkerwebModule
def get_object
Proc.new { BunkerwebWorker.new }
end
module_function :get_object
end
class Kernel::PluginConfiguration
include BunkerwebModule
@@loaded_plugins[BunkerwebWorker::MODULE_ID] = BunkerwebModule.get_object
end

View File

@@ -0,0 +1 @@
7

View File

@@ -0,0 +1,14 @@
---
- name: Install Bunkerweb on localhost
hosts: localhost
connection: local
become: true
gather_facts: false
environment:
LANG: en_US.UTF-8
LC_ALL: en_US.UTF-8
tasks:
- name: Install bunkerweb
ansible.builtin.dnf:
name: bunkerweb
state: present

View File

@@ -49,7 +49,7 @@
# Конфигурируем Nginx для Passenger # Конфигурируем Nginx для Passenger
- name: Create passenger.conf - name: Create passenger.conf
ansible.builtin.copy: ansible.builtin.copy:
dest: /etc/nginx/conf.d/passenger.conf dest: /usr/local/hestia/nginx-system/etc/nginx/conf.d/passenger.conf
content: | content: |
passenger_root /usr/share/ruby/vendor_ruby/phusion_passenger/locations.ini; passenger_root /usr/share/ruby/vendor_ruby/phusion_passenger/locations.ini;
passenger_ruby /usr/bin/ruby; passenger_ruby /usr/bin/ruby;
@@ -59,11 +59,11 @@
passenger_env_var PASSENGER_DOWNLOAD_NATIVE_SUPPORT_BINARY 0; passenger_env_var PASSENGER_DOWNLOAD_NATIVE_SUPPORT_BINARY 0;
- name: Create passenger_includer.conf - name: Create passenger_includer.conf
ansible.builtin.copy: ansible.builtin.copy:
dest: /etc/nginx/conf.d/main/passenger.conf dest: /usr/local/hestia/nginx-system/etc/nginx/conf.d/main/passenger.conf
content: | content: |
load_module modules/ngx_http_passenger_module.so; load_module modules/ngx_http_passenger_module.so;
# Перезапускаем Nginx # Перезапускаем Nginx
- name: Restart nginx service - name: Restart nginx service
ansible.builtin.service: ansible.builtin.service:
name: nginx name: nginx-system
state: restarted state: restarted

View File

@@ -25,14 +25,14 @@
# Удаляем конфигурационные файлы Nginx # Удаляем конфигурационные файлы Nginx
- name: Remove passenger.conf - name: Remove passenger.conf
ansible.builtin.file: ansible.builtin.file:
path: /etc/nginx/conf.d/passenger.conf path: /usr/local/hestia/nginx-system/etc/nginx/conf.d/passenger.conf
state: absent state: absent
- name: Remove passenger_includer.conf - name: Remove passenger_includer.conf
ansible.builtin.file: ansible.builtin.file:
path: /etc/nginx/conf.d/main/passenger.conf path: /usr/local/hestia/nginx-system/etc/nginx/conf.d/main/passenger.conf
state: absent state: absent
# Перезапускаем Nginx (необязательно, но полезно) # Перезапускаем Nginx (необязательно, но полезно)
- name: Restart nginx service - name: Restart nginx service
ansible.builtin.service: ansible.builtin.service:
name: nginx name: nginx-system
state: restarted state: restarted

View File

@@ -1,6 +1,6 @@
#!/opt/brepo/ruby33/bin/ruby #!/opt/brepo/ruby33/bin/ruby
class EmptyWorker < Kernel::ModuleCoreWorker class PHPWorker < Kernel::ModuleCoreWorker
MODULE_ID = "php_brepo_modules" MODULE_ID = "php_brepo_modules"
def info def info
@@ -241,16 +241,16 @@ class EmptyWorker < Kernel::ModuleCoreWorker
implements IPluginInterface implements IPluginInterface
end end
module EmptyModule module PHPModule
def get_object def get_object
Proc.new { EmptyWorker.new } Proc.new { PHPWorker.new }
end end
module_function :get_object module_function :get_object
end end
class Kernel::PluginConfiguration class Kernel::PluginConfiguration
include EmptyModule include PHPModule
@@loaded_plugins[EmptyWorker::MODULE_ID] = EmptyModule.get_object @@loaded_plugins[PHPWorker::MODULE_ID] = PHPModule.get_object
end end

View File

@@ -0,0 +1,122 @@
#!/opt/brepo/ruby33/bin/ruby
require 'pathname'
require 'fileutils'
require 'digest'
class UpdateWorker < Kernel::ModuleCoreWorker
MODULE_ID = "update_module"
def info
{
ID: 6,
NAME: MODULE_ID,
DESCR: "Module for updating HestiaCP data and templates",
REQ: "",
CONF: "yes",
}
end
def file_changed?(new_file, old_file)
return true unless File.exist?(old_file)
new_hash = Digest::SHA256.file(new_file).hexdigest
old_hash = Digest::SHA256.file(old_file).hexdigest
new_hash != old_hash
end
def get_templates_map()
{ :templates=>
[
{:new=>"/usr/local/hestia/install/rpm/templates/web/awstats", :old=>"/usr/local/hestia/data/templates/web/awstats"},
{:new=>"/usr/local/hestia/install/rpm/templates/web/httpd", :old=>"/usr/local/hestia/data/templates/web/httpd"},
{:new=>"/usr/local/hestia/install/rpm/templates/web/nginx", :old=>"/usr/local/hestia/data/templates/web/nginx"},
{:new=>"/usr/local/hestia/install/rpm/templates/web/php-fpm", :old=>"/usr/local/hestia/data/templates/web/php-fpm"}
]
}
end
# New helper method to get list of changed template files
def get_changed_template_files
templates_map = get_templates_map()[:templates]
result = []
templates_map.each do |tpl|
new_dir = tpl[:new]
old_dir = tpl[:old]
Dir.glob(File.join(new_dir, '**', '*')).each do |new_file|
next if File.directory?(new_file)
rel_path = Pathname.new(new_file).relative_path_from(Pathname.new(new_dir)).to_s
old_file = File.join(old_dir, rel_path)
result << [new_dir, new_file, old_file] if file_changed?(new_file, old_file)
end
end
result
end
def command(args)
return log_return("Not enough arguments. Needed command") if args.length < 1
log_file = get_log
m_command = args[0].strip
case m_command
when "synctemplates"
result = get_changed_template_files
result.each do |new_dir, new_file, old_file|
if !File.exist?(old_file)
FileUtils.cp(new_file, old_file)
else
stat = File.stat(old_file)
uid = stat.uid
gid = stat.gid
mode = stat.mode & 0o7777
FileUtils.cp(new_file, old_file)
File.chown(uid, gid, old_file)
File.chmod(mode, old_file)
end
end
ACTION_OK
when "listsynctemplates"
format = (args[1].nil? ? "shell" : args[1].strip)
list = get_changed_template_files
result = []
result = list.map do |new_dir, new_file, old_file|
file_name = Pathname.new(new_file).relative_path_from(Pathname.new(new_dir)).to_s
dir_name = File.basename(new_dir)
relative_path = Pathname.new(new_file).relative_path_from(Pathname.new(new_dir)).to_s
file_name = File.join(dir_name, relative_path)
{
"FILE_NAME" => file_name,
"NEW_SIZE" => File.size(new_file),
"OLD_SIZE" => File.exist?(old_file) ? File.size(old_file) : "-"
}
end
hestia_print_array_of_hashes(result, format, "FILE_NAME,NEW_SIZE,OLD_SIZE")
ACTION_OK
when "help"
puts "#{$0} update_module COMMAND [json|csv|plain]"
puts "COMMANDS:"
puts " synctemplates - sync web templates"
puts " listsynctemplates - show changed web templates"
puts " help - help"
ACTION_OK
else
log_return("Unknown command. #{args}")
end
end
implements IPluginInterface
end
module UpdateModule
def get_object
Proc.new { UpdateWorker.new }
end
module_function :get_object
end
class Kernel::PluginConfiguration
include UpdateModule
@@loaded_plugins[UpdateWorker::MODULE_ID] = UpdateModule.get_object
end

View File

@@ -77,6 +77,10 @@ class File
end end
end end
def hestia_print_info_message_to_cli(error_message)
puts "Info: #{error_message}"
end
def hestia_print_error_message_to_cli(error_message) def hestia_print_error_message_to_cli(error_message)
puts "Error: #{error_message}" puts "Error: #{error_message}"
end end
@@ -315,3 +319,80 @@ def hestia_save_file_key_pair(file, key, value)
end end
end end
end end
def hestia_change_sys_config_value(key, value)
# Privileged access check
hestia_check_privileged_user unless Process.uid == 0
config_file = "/usr/local/hestia/conf/hestia.conf"
if File.exist?(config_file)
# First pass: read entire file to check if key exists and get all lines
content = nil
File.open(config_file, "r") do |f|
content = f.read
end
if content
lines = content.split("\n")
# Check if key exists in the configuration file
existing_line_index = -1
lines.each_with_index do |line, idx|
line_stripped = line.strip
# Skip comment lines
next if line_stripped.start_with?("#")
next if line_stripped.empty?
key_match = line_stripped.match(/^\s*#{Regexp.escape(key)}='\s*(.*?)\s*$/)
if key_match
existing_line_index = idx + 1
break
end
end
if existing_line_index.nil? || existing_line_index == -1
# Key doesn't exist - append new line to file
File.open(config_file, "a") do |append_f|
append_f.puts("#{key}='#{value}'")
end
OK
else
# Key exists - update value using Ruby operators (in-place edit)
# Use temporary file for safety and atomic replacement
temp_file = "#{config_file}.tmp"
# Second pass: rebuild the content with updated value
new_lines = []
lines.each do |line|
line_stripped = line.strip
# Skip comment lines
next if line_stripped.start_with?("#")
next if line_stripped.empty?
# Match and replace the key-value pair
if line.match(/^\s*#{Regexp.escape(key)}='[^']*'/)
new_lines << "#{key}='#{value}'"
else
new_lines << line
end
end
File.open(temp_file, "w") do |output_f|
new_lines.each { |l| output_f.puts(l) }
end
# Atomic file replacement
File.rename(temp_file, config_file)
OK
end
else
OK
end
else
check_result error_code: E_NOTEXIST, error_message: "Configuration file #{config_file} does not exist"
end
end

View File

@@ -1,9 +1,9 @@
passdb { passdb {
driver = passwd-file driver = passwd-file
args = scheme=MD5-CRYPT username_format=%n /etc/exim4/domains/%d/passwd args = scheme=MD5-CRYPT username_format=%n /etc/exim/domains/%d/passwd
} }
userdb { userdb {
driver = passwd-file driver = passwd-file
args = username_format=%n /etc/exim4/domains/%d/passwd args = username_format=%n /etc/exim/domains/%d/passwd
} }

View File

@@ -1,96 +0,0 @@
# It is split into several files forming the configuration hierarchy outlined
# below, all located in the /etc/apache2/ directory:
#
# /etc/apache2/
# |-- apache2.conf
# | `-- ports.conf
# |-- mods-enabled
# | |-- *.load
# | `-- *.conf
# |-- conf.d
# | `-- *
# Global configuration
PidFile ${APACHE_PID_FILE}
Timeout 30
KeepAlive Off
MaxKeepAliveRequests 100
KeepAliveTimeout 10
<IfModule mpm_prefork_module>
StartServers 8
MinSpareServers 5
MaxSpareServers 20
ServerLimit 256
MaxClients 200
MaxRequestsPerChild 4000
</IfModule>
<IfModule mpm_worker_module>
StartServers 2
MinSpareThreads 25
MaxSpareThreads 75
ThreadLimit 64
ThreadsPerChild 25
MaxClients 200
MaxRequestsPerChild 4000
</IfModule>
<IfModule mpm_event_module>
StartServers 2
MinSpareThreads 25
MaxSpareThreads 75
ThreadLimit 64
ThreadsPerChild 25
MaxClients 200
MaxRequestsPerChild 4000
</IfModule>
# These need to be set in /etc/apache2/envvars
User ${APACHE_RUN_USER}
Group ${APACHE_RUN_GROUP}
#User www-data
#Group www-data
AccessFileName .htaccess
<Files ~ "^\.ht">
Order allow,deny
Deny from all
Satisfy all
</Files>
DefaultType None
HostnameLookups Off
ErrorLog ${APACHE_LOG_DIR}/error.log
LogLevel warn
# Include module configuration:
Include mods-enabled/*.load
Include mods-enabled/*.conf
# Include list of ports to listen on and which to use for name based vhosts
Include ports.conf
LogFormat "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined
LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined
LogFormat "%h %l %u %t \"%r\" %>s %O" common
LogFormat "%{Referer}i -> %U" referer
LogFormat "%{User-agent}i" agent
LogFormat "%b" bytes
IncludeOptional conf.d/*.conf
IncludeOptional conf.d/domains/webmail.*.conf
IncludeOptional conf.d/domains/*.conf
# Include the virtual host configurations:
#Include sites-enabled/
ErrorDocument 403 /error/403.html
ErrorDocument 404 /error/404.html
ErrorDocument 500 /error/50x.html
ErrorDocument 501 /error/50x.html
ErrorDocument 502 /error/50x.html
ErrorDocument 503 /error/50x.html
ErrorDocument 506 /error/50x.html

View File

@@ -1,5 +0,0 @@
<IfModule mpm_event_module>
<FilesMatch \.php$>
SetHandler "proxy:fcgi://127.0.0.1:9000"
</FilesMatch>
</IfModule>

View File

@@ -1,7 +0,0 @@
Listen 127.0.0.1:8081
<Location /server-status>
SetHandler server-status
Order deny,allow
Deny from all
Allow from 127.0.0.1
</Location>

View File

@@ -1,18 +0,0 @@
<VirtualHost directIP:directPORT>
ServerName directIP
DocumentRoot /var/www/html/
Alias /error/ /var/www/document_errors/
</VirtualHost>
<VirtualHost directIP:directSSLPORT>
ServerName directIP
DocumentRoot /var/www/html/
Alias /error/ /var/www/document_errors/
SSLEngine on
SSLVerifyClient none
SSLCertificateFile /usr/local/hestia/ssl/certificate.crt
SSLCertificateKeyFile /usr/local/hestia/ssl/certificate.key
</VirtualHost>

View File

@@ -1,12 +0,0 @@
// This is the primary configuration file for the BIND DNS server named.
//
// Please read /usr/share/doc/bind9/README.Debian.gz for information on the
// structure of BIND configuration files in Debian, *BEFORE* you customize
// this configuration file.
//
// If you are just adding zones, please do that in /etc/bind/named.conf.local
include "/etc/bind/named.conf.options";
include "/etc/bind/named.conf.local";
include "/etc/bind/named.conf.default-zones";

View File

@@ -1,24 +0,0 @@
options {
directory "/var/cache/bind";
// If there is a firewall between you and nameservers you want
// to talk to, you may need to fix the firewall to allow multiple
// ports to talk. See http://www.kb.cert.org/vuls/id/800113
// If your ISP provided one or more IP addresses for stable
// nameservers, you probably want to use them as forwarders.
// Uncomment the following block, and insert the addresses replacing
// the all-0's placeholder.
// forwarders {
// 0.0.0.0;
// };
//========================================================================
// If BIND logs error messages about the root key being expired,
// you will need to update your keys. See https://www.isc.org/bind-keys
//========================================================================
dnssec-validation auto;
auth-nxdomain no;
allow-recursion { 127.0.0.1; ::1; };
allow-transfer {"none";};
hostname none;
server-id none;
version none;
};

View File

@@ -1,88 +0,0 @@
#Automatically Generated by clamav-daemon postinst
#To reconfigure clamd run #dpkg-reconfigure clamav-daemon
#Please read /usr/share/doc/clamav-daemon/README.Debian.gz for details
LocalSocket /run/clamav/clamd.ctl
FixStaleSocket true
LocalSocketGroup clamav
LocalSocketMode 666
# TemporaryDirectory is not set to its default /tmp here to make overriding
# the default with environment variables TMPDIR/TMP/TEMP possible
User clamav
ScanMail true
ScanArchive true
ArchiveBlockEncrypted false
MaxDirectoryRecursion 15
FollowDirectorySymlinks false
FollowFileSymlinks false
ReadTimeout 180
MaxThreads 12
MaxConnectionQueueLength 15
LogSyslog false
LogRotate true
LogFacility LOG_LOCAL6
LogClean false
LogVerbose true
PreludeEnable no
PreludeAnalyzerName ClamAV
DatabaseDirectory /var/lib/clamav
OfficialDatabaseOnly false
SelfCheck 3600
Foreground false
Debug false
ScanPE true
MaxEmbeddedPE 10M
ScanOLE2 true
ScanPDF true
ScanHTML true
MaxHTMLNormalize 10M
MaxHTMLNoTags 2M
MaxScriptNormalize 5M
MaxZipTypeRcg 1M
ScanSWF true
ExitOnOOM false
LeaveTemporaryFiles false
AlgorithmicDetection true
ScanELF true
IdleTimeout 30
CrossFilesystems true
PhishingSignatures true
PhishingScanURLs true
PhishingAlwaysBlockSSLMismatch false
PhishingAlwaysBlockCloak false
PartitionIntersection false
DetectPUA false
ScanPartialMessages false
HeuristicScanPrecedence false
StructuredDataDetection false
CommandReadTimeout 5
SendBufTimeout 200
MaxQueue 100
ExtendedDetectionInfo true
OLE2BlockMacros false
AllowAllMatchScan true
ForceToDisk false
DisableCertCheck false
DisableCache false
MaxScanTime 120000
MaxScanSize 100M
MaxFileSize 25M
MaxRecursion 16
MaxFiles 10000
MaxPartitions 50
MaxIconsPE 100
PCREMatchLimit 10000
PCRERecMatchLimit 5000
PCREMaxFileSize 25M
ScanXMLDOCS true
ScanHWP3 true
MaxRecHWP3 16
StreamMaxLength 25M
LogFile /var/log/clamav/clamav.log
LogTime true
LogFileUnlock false
LogFileMaxSize 0
Bytecode true
BytecodeSecurity TrustSigned
BytecodeTimeout 60000
PidFile /run/clamav/clamd.pid
OnAccessMaxFileSize 5M

View File

@@ -1,30 +0,0 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v1.4.12 (GNU/Linux)
mQENBFJIGbEBCAC8SHOOFo7iDTbnC2GhNZ+uBGCh226Dn1QPoFZNFM/DNakHZ6rD
G3wzr8++eKz4fJual/VLllE2N9XDPuxbozb3LLkcyY1WzJqtIXbXhFGQ/SuIeT+x
QY90XU6t2Ckze2c+zUniAWmJ8GSyVmXOoc9JxAQ1u47wvGXLzrjWXc8u8PNRYXuf
fZplTL+dFu9P0d6lP8FGsV+r9wXvvazpRTz3+H8PKrGCYT55ZQIEdG9Jgamylto2
oVPFXkwGML+TLw6oeCIBuz2y2vtivphW4MJ3ifQjDj7k3n+DTIxfDFs8lB6VRhhY
2nMHCrcZC6U2mhmXmr6O4s1fu6irBVx05ejPABEBAAG0IFNlcmdoZXkgUm9kaW4g
PHNraWRAdmVzdGFjcC5jb20+iQE4BBMBAgAiBQJSSBmxAhsDBgsJCAcDAgYVCAIJ
CgsEFgIDAQIeAQIXgAAKCRBCxbITCh93FPdqB/93GjV9g+wBfeZYLHQK9MDU2wBb
VloYOJJae6IvYKYQVAJayD3PbHdpxrF8s9e23vdnmb9jKu6jX6oV54EIyqP2HPiN
QYc8wcea+eSHerznBixCtoQh8mtdWGFeN71zU/ig7L5qlOVF/EmxDVZTFUeivFxh
IV6qyBnktQKktE45585yKZyyLtfGoXA54DGK69OtJFh+wdkKEMmUXocMl7wUrxW6
Cx2CuKeEXEgvwu8mRHQi3S3T9XP456qWEn5dWyMVcP660IzEuZfSJApZusNK7zG3
WMy0/EuX7xHNY3mcNxTOUN1LsO7iHnhHD9+iKWJo9parGkMZzc92MpjDK/g7uQEN
BFJIGbEBCAC7k5QEA9WQM7E3ceNaeLMrA9lXfuzaNCcySq7ONdVAa5PxzbSKdHvz
QFoL1VFqBTYQ038lbil1XqnoM0zvIfAI3LcpS8sq92El/vPxp6jZh2Ari9Uw7x95
k2cZMgI67g+zQMGdjVRA155nFQRCgg000xU4F7JA6+WsuLlVUmccsDv7YWJExMtC
YPxiuz5DFu8RALnw4Ckts+dbwsrcvUHhkm9b6RAsdCKjjRpUZjLgdltjH83gUVvt
i1YmdjjsVpt95dtsaG+ad852g/Rk8EdxNMkjPF6HLA67CLADP9wYaj80yPcPtylS
ycvPtcclVeHkFBRVM8xZpQd4iD19MWI1ABEBAAGJAR8EGAECAAkFAlJIGbECGwwA
CgkQQsWyEwofdxQ7tQgAhB0FwTs7L8Qr63DHC2yAnXVxgtTAY1/36CccNXVculyR
+EkLcwahms9AKhz7eQb+Mud+5vH0GRohLp2npgO38CjVUfIP5d+Y6dsthmrkF6p8
XdV1dVK9vWX+i/YZSw/Mded30Cq4P2Yhq9EaemMT0rtli8lz2NnkZ9dFJZk1lzJC
CZmRpbjSNWqRU4f7qyh21lYk/OC/0XE8fh8CaO23TZ+6gBionoCztwb7NyC9OArN
qYlNnbmh9iNqdblykPS3bkjf34n2xyMgnIehNrM89tk8PY4UfNPhgT1TMD9W3Svq
ynNZvLuF/FIDwDeC1qcfjGbfDn9fXO/lMIIRooQYKQ==
=J2HJ
-----END PGP PUBLIC KEY BLOCK-----

View File

@@ -1,2 +0,0 @@
bl.spamcop.net
zen.spamhaus.org

View File

@@ -1,476 +0,0 @@
######################################################################
# #
# Exim configuration file for Hestia Control Panel #
# #
######################################################################
#SPAMASSASSIN = yes
#SPAM_SCORE = 50
#SPAM_REJECT_SCORE = 100
#CLAMD = yes
smtp_banner = $smtp_active_hostname
smtp_active_hostname = ${lookup dnsdb{>: defer_never,ptr=$interface_address}{${listextract{1}{$value}}}{$primary_hostname}}
add_environment = <; PATH=/bin:/usr/bin
keep_environment =
disable_ipv6 = true
smtputf8_advertise_hosts =
domainlist local_domains = dsearch;/etc/exim4/domains/
domainlist relay_to_domains = dsearch;/etc/exim4/domains/
hostlist relay_from_hosts = 127.0.0.1
hostlist whitelist = net-iplsearch;/etc/exim4/white-blocks.conf
hostlist spammers = net-iplsearch;/etc/exim4/spam-blocks.conf
no_local_from_check
untrusted_set_sender = *
acl_smtp_connect = acl_check_spammers
acl_smtp_mail = acl_check_mail
acl_smtp_rcpt = acl_check_rcpt
acl_smtp_data = acl_check_data
acl_smtp_mime = acl_check_mime
.ifdef SPAMASSASSIN
spamd_address = 127.0.0.1 783
.endif
.ifdef CLAMD
av_scanner = clamd: /run/clamav/clamd.ctl
.endif
log_selector = +tls_sni
tls_advertise_hosts = *
# We test that $tls_in_sni is a valid domain, by an arbitrary email address foo@domain.tld .
# Then, we extract the domain with a function that would fail if the email address is invalid.
# If the certificate exists, we will use it, otherwise the default certificate in /etc/ssl will be used.
tls_certificate = \
${if and {\
{ eq {${domain:foo@$tls_in_sni}} {$tls_in_sni}}\
{ exists{/usr/local/hestia/ssl/mail/$tls_in_sni.crt} }\
}\
{/usr/local/hestia/ssl/mail/$tls_in_sni.crt}\
{/usr/local/hestia/ssl/certificate.crt}\
}
tls_privatekey = \
${if and {\
{ eq {${domain:foo@$tls_in_sni}} {$tls_in_sni}}\
{ exists{/usr/local/hestia/ssl/mail/$tls_in_sni.key} }\
}\
{/usr/local/hestia/ssl/mail/$tls_in_sni.key}\
{/usr/local/hestia/ssl/certificate.key}\
}
daemon_smtp_ports = 25 : 465 : 587
tls_on_connect_ports = 465
tls_require_ciphers = PERFORMANCE:-RSA:-VERS-ALL:+VERS-TLS1.2:+VERS-TLS1.3:%SERVER_PRECEDENCE
never_users = root
host_lookup = *
rfc1413_hosts = *
rfc1413_query_timeout = 0s
ignore_bounce_errors_after = 2d
timeout_frozen_after = 7d
DKIM_DOMAIN = ${lc:${domain:$h_from:}}
DKIM_FILE = /etc/exim4/domains/${lookup{${lc:${domain:$h_from:}}}dsearch{/etc/exim4/domains/}}/dkim.pem
DKIM_PRIVATE_KEY = ${if exists{DKIM_FILE}{DKIM_FILE}{0}}
OUTGOING_IP = /etc/exim4/domains/${lookup{$sender_address_domain}dsearch{/etc/exim4/domains}}/ip
SMTP_RELAY_FILE = ${if exists{/etc/exim4/domains/${lookup{$sender_address_domain}dsearch{/etc/exim4/domains}}/smtp_relay.conf}{/etc/exim4/domains/${lookup{$sender_address_domain}dsearch{/etc/exim4/domains}}/smtp_relay.conf}{/etc/exim4/smtp_relay.conf}}
SMTP_RELAY_HOST = ${lookup{host}lsearch{SMTP_RELAY_FILE}}
SMTP_RELAY_PORT = ${lookup{port}lsearch{SMTP_RELAY_FILE}}
SMTP_RELAY_USER = ${lookup{user}lsearch{SMTP_RELAY_FILE}}
SMTP_RELAY_PASS = ${lookup{pass}lsearch{SMTP_RELAY_FILE}}
# Custom Filter
system_filter = /etc/exim4/system.filter
system_filter_user = Debian-exim
######################################################################
# ACL CONFIGURATION #
# Specifies access control lists for incoming SMTP mail #
######################################################################
acl_not_smtp = acl_not_smtp
begin acl
# Limit per user for PHP scripts
acl_not_smtp:
deny message = Website of user $authenticated_id is sending too many emails - rate overlimit = $sender_rate / $sender_rate_period
ratelimit = 200 / 1h / $authenticated_id
warn ratelimit = 100 / 1h / strict / $authenticated_id
log_message = Sender rate [limitlog]: log / account / $authenticated_id / $sender_rate / $sender_rate_period
accept
acl_check_spammers:
accept hosts = +whitelist
drop message = Your host in blacklist on this server.
log_message = Host in blacklist
hosts = +spammers
accept
acl_check_mail:
deny condition = ${if eq{$sender_helo_name}{}}
message = HELO required before MAIL
drop !authenticated = *
message = Helo name contains an IP address (HELO was $sender_helo_name) and not is valid
condition = ${if match{$sender_helo_name}{\N((\d{1,3}[.-]\d{1,3}[.-]\d{1,3}[.-]\d{1,3})|([0-9a-f]{8})|([0-9A-F]{8}))\N}{yes}{no}}
condition = ${if match {${lookup dnsdb{>: defer_never,ptr=$sender_host_address}}\}{$sender_helo_name}{no}{yes}}
delay = 45s
drop !authenticated = *
condition = ${if isip{$sender_helo_name}}
message = Access denied - Invalid HELO name (See RFC2821 4.1.3)
drop !authenticated = *
condition = ${if eq{[$interface_address]}{$sender_helo_name}}
message = $interface_address is _my_ address
accept
acl_check_rcpt:
accept hosts = :
# Limit per email account for SMTP auhenticated users
deny message = Email account $authenticated_id is sending too many emails - rate overlimit = $sender_rate / $sender_rate_period
set acl_c_msg_limit = ${if exists{/etc/exim4/domains/${lookup{${domain:$authenticated_id}}dsearch{/etc/exim4/domains/}}/limits}{${lookup {$authenticated_id} lsearch{/etc/exim4/domains/${lookup{${domain:$authenticated_id}}dsearch{/etc/exim4/domains/}}/limits}{$value}{${readfile{/etc/exim4/limit.conf}}}}}{${readfile{/etc/exim4/limit.conf}}} }
ratelimit = $acl_c_msg_limit / 1h / strict/ $authenticated_id
warn ratelimit = ${eval:$acl_c_msg_limit / 2} / 1h / strict / $authenticated_id
log_message = Sender rate [limitlog]: log / email / $authenticated_id / $sender_rate / $sender_rate_period
deny message = Restricted characters in address
domains = +local_domains
local_parts = ^[.] : ^.*[@%!/|]
deny message = Restricted characters in address
domains = !+local_domains
local_parts = ^[./|] : ^.*[@%!] : ^.*/\\.\\./
require verify = sender
accept hosts = +relay_from_hosts
control = submission
accept authenticated = *
control = submission/domain=
deny message = Rejected because $sender_host_address is in a black list at $dnslist_domain\n$dnslist_text
hosts = !+whitelist
dnslists = ${readfile {/etc/exim4/dnsbl.conf}{:}}
require message = relay not permitted
domains = +local_domains : +relay_to_domains
deny message = smtp auth required
sender_domains = +local_domains
!authenticated = *
require verify = recipient
.ifdef CLAMD
warn set acl_m0 = no
warn condition = ${if exists {/etc/exim4/domains/$domain/antivirus}{yes}{no}}
set acl_m0 = yes
.endif
.ifdef SPAMASSASSIN
warn set acl_m1 = no
set acl_m3 = no
warn condition = ${if exists {/etc/exim4/domains/$domain/antispam}{yes}{no}}
set acl_m1 = yes
warn condition = ${if exists {/etc/exim4/domains/$domain/reject_spam}{yes}{no}}
set acl_m3 = yes
.endif
accept
acl_check_data:
.ifdef CLAMD
deny message = Message contains a virus ($malware_name) and has been rejected
malware = */defer_ok
condition = ${if eq{$acl_m0}{yes}{yes}{no}}
.endif
.ifdef SPAMASSASSIN
warn !authenticated = *
hosts = !+relay_from_hosts
condition = ${if < {$message_size}{1024K}}
condition = ${if eq{$acl_m1}{yes}{yes}{no}}
spam = debian-spamd:true/defer_ok
add_header = X-Spam-Score: $spam_score_int
add_header = X-Spam-Bar: $spam_bar
add_header = X-Spam-Report: $spam_report
set acl_m2 = $spam_score_int
warn condition = ${if !eq{$acl_m2}{} {yes}{no}}
condition = ${if >{$acl_m2}{SPAM_SCORE} {yes}{no}}
add_header = X-Spam-Status: Yes
message = SpamAssassin detected spam (from $sender_address to $recipients).
# Deny spam at high score if spam score > SPAM_REJECT_SCORE and delete_spam is enabled
deny message = This message scored $spam_score spam points
spam = debian-spamd:true
condition = ${if eq{$acl_m3}{yes}{yes}{no}}
condition = ${if >{$spam_score_int}{SPAM_REJECT_SCORE}{1}{0}}
.endif
accept
acl_check_mime:
deny message = Blacklisted file extension detected
condition = ${if match {${lc:$mime_filename}}{\N(\.ace|\.ade|\.adp|\.app|\.arj|\.asp|\.aspx|\.asx|\.bas|\.bat|\.cab|\.cer|\.chm|\.cmd|\.cnt|\.com|\.cpl|\.crt|\.csh|\.der|\.diagcab|\.dll|\.efi|\.exe|\.fla|\.fon|\.fxp|\.gadget|\.grp|\.hlp|\.hpj|\.hta|\.htc|\.img|\.inf|\.ins|\.iso|\.isp|\.its|\.jar|\.jnlp|\.js|\.jse|\.ksh|\.lib|\.lnk|\.mad|\.maf|\.mag|\.mam|\.maq|\.mar|\.mas|\.mat|\.mau|\.mav|\.maw|\.mcf|\.mda|\.mdb|\.mde|\.mdt|\.mdw|\.mdz|\.msc|\.msh|\.msh1|\.msh1xml|\.msh2|\.msh2xml|\.mshxml|\.msi|\.msp|\.mst|\.msu|\.ops|\.osd|\.pcd|\.pif|\.pl|\.plg|\.prf|\.prg|\.printerexport|\.ps1|\.ps1xml|\.ps2|\.ps2xml|\.psc1|\.psc2|\.psd1|\.psdm1|\.pst|\.py|\.pyc|\.pyo|\.pyw|\.pyz|\.pyzw|\.reg|\.scf|\.scr|\.sct|\.sfx|\.shb|\.shs|\.swf|\.sys|\.theme|\.tmp|\.ttf|\.url|\.vb|\.vba|\.vbe|\.vbp|\.vbs|\.vhd|\.vhdx|\.vsmacros|\.vsw|\.vxd|\.webpnp|\.website|\.wim|\.ws|\.wsc|\.wsf|\.wsh|\.xbap|\.xll|\.xnk)$\N}{1}{0}}
accept
######################################################################
# AUTHENTICATION CONFIGURATION #
######################################################################
begin authenticators
smtp_relay_login:
driver = plaintext
public_name = LOGIN
hide client_send = : SMTP_RELAY_USER : SMTP_RELAY_PASS
dovecot_plain:
driver = dovecot
public_name = PLAIN
server_socket = /run/dovecot/auth-client
server_set_id = $auth1
dovecot_login:
driver = dovecot
public_name = LOGIN
server_socket = /run/dovecot/auth-client
server_set_id = $auth1
######################################################################
# ROUTERS CONFIGURATION #
# Specifies how addresses are handled #
######################################################################
begin routers
send_via_unauthenticated_smtp_relay:
driver = manualroute
address_data = SMTP_RELAY_HOST:SMTP_RELAY_PORT
domains = !+local_domains
require_files = SMTP_RELAY_FILE
condition = ${if eq{SMTP_RELAY_USER}{}}
transport = remote_smtp
route_list = * ${extract{1}{:}{$address_data}}::${extract{2}{:}{$address_data}}
no_more
no_verify
send_via_smtp_relay:
driver = manualroute
address_data = SMTP_RELAY_HOST:SMTP_RELAY_PORT
domains = !+local_domains
require_files = SMTP_RELAY_FILE
transport = smtp_relay_smtp
route_list = * ${extract{1}{:}{$address_data}}::${extract{2}{:}{$address_data}}
no_more
no_verify
dnslookup:
driver = dnslookup
domains = !+local_domains
transport = remote_smtp
no_more
userforward:
driver = redirect
check_local_user
file = $home/.forward
require_files = ${local_part}:+${home}/.forward
domains = +local_domains
allow_filter
no_verify
no_expn
check_ancestor
file_transport = address_file
pipe_transport = address_pipe
reply_transport = address_reply
procmail:
driver = accept
check_local_user
require_files = ${local_part}:+${home}/.procmailrc:/usr/bin/procmail
transport = procmail
no_verify
autoreplay:
driver = accept
require_files = /etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/autoreply.${local_part}.msg
condition = ${if exists{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/autoreply.${local_part}.msg}{yes}{no}}
retry_use_local_part
transport = userautoreply
unseen
aliases:
driver = redirect
headers_add = X-redirected: yes
data = ${extract{1}{:}{${lookup{$local_part@$domain}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/aliases}}}}
require_files = /etc/exim4/domains/$domain/aliases
redirect_router = dnslookup
pipe_transport = address_pipe
unseen
localuser_fwd_only:
driver = accept
transport = devnull
condition = ${if exists{/etc/exim4/domains/$domain/fwd_only}{${lookup{$local_part}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/fwd_only}{true}{false}}}}
localuser_spam:
driver = accept
transport = local_spam_delivery
condition = ${if eq {${if match{$h_X-Spam-Status:}{\N^Yes\N}{yes}{no}}} {${lookup{$local_part}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/passwd}{yes}{no_such_user}}}}
localuser:
driver = accept
transport = local_delivery
condition = ${lookup{$local_part}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/passwd}{true}{false}}
catchall:
driver = redirect
headers_add = X-redirected: yes
require_files = /etc/exim4/domains/$domain/aliases
data = ${extract{1}{:}{${lookup{*@$domain}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/aliases}}}}
file_transport = local_delivery
redirect_router = dnslookup
condition = ${lookup{$local_part@$domain}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/aliases}{false}{true}}
terminate_alias:
driver = accept
transport = devnull
condition = ${lookup{$local_part@$domain}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/aliases}{true}{false}}
######################################################################
# TRANSPORTS CONFIGURATION #
######################################################################
begin transports
smtp_relay_smtp:
driver = smtp
hosts_require_auth = $host_address
hosts_require_tls = $host_address
remote_smtp:
driver = smtp
helo_data = ${lookup dnsdb{>: defer_never,ptr=$sending_ip_address}{${listextract{1}{$value}}}{$primary_hostname}}
dkim_domain = DKIM_DOMAIN
dkim_selector = mail
dkim_private_key = DKIM_PRIVATE_KEY
dkim_canon = relaxed
dkim_strict = 0
hosts_try_fastopen = !*.l.google.com
interface = ${if exists{OUTGOING_IP}{${readfile{OUTGOING_IP}}}}
procmail:
driver = pipe
command = "/usr/bin/procmail -d $local_part"
return_path_add
delivery_date_add
envelope_to_add
user = $local_part
initgroups
return_output
local_delivery:
driver = appendfile
maildir_format
maildir_use_size_file
user = ${extract{2}{:}{${lookup{$local_part}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/passwd}}}}
group = mail
create_directory
directory_mode = 770
mode = 660
use_lockfile = no
delivery_date_add
envelope_to_add
return_path_add
directory = "${extract{5}{:}{${lookup{$local_part}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/passwd}}}}/mail/${lookup{$domain}dsearch{/etc/exim4/domains/}}/${lookup{$local_part}dsearch{${extract{5}{:}{${lookup{$local_part}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/passwd}}}}/mail/${lookup{$domain}dsearch{/etc/exim4/domains/}}}}"
quota = ${extract{6}{:}{${lookup{$local_part}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/passwd}}}}M
quota_warn_threshold = 75%
local_spam_delivery:
driver = appendfile
maildir_format
maildir_use_size_file
user = ${extract{2}{:}{${lookup{$local_part}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/passwd}}}}
group = mail
create_directory
directory_mode = 770
mode = 660
use_lockfile = no
delivery_date_add
envelope_to_add
return_path_add
directory = "${extract{5}{:}{${lookup{$local_part}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/passwd}}}}/mail/${lookup{$domain}dsearch{/etc/exim4/domains/}}/${lookup{$local_part}dsearch{${extract{5}{:}{${lookup{$local_part}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/passwd}}}}/mail/${lookup{$domain}dsearch{/etc/exim4/domains/}}}}/.Spam"
quota = ${extract{6}{:}{${lookup{$local_part}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/passwd}}}}M
quota_directory = "${extract{5}{:}{${lookup{$local_part}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/passwd}}}}/mail/${lookup{$domain}dsearch{/etc/exim4/domains/}}/${lookup{$local_part}dsearch{${extract{5}{:}{${lookup{$local_part}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/passwd}}}}/mail/${lookup{$domain}dsearch{/etc/exim4/domains/}}}}"
quota_warn_threshold = 75%
address_pipe:
driver = pipe
return_output
address_file:
driver = appendfile
delivery_date_add
envelope_to_add
return_path_add
address_reply:
driver = autoreply
userautoreply:
driver = autoreply
file = /etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/autoreply.${extract{1}{:}{${lookup{$local_part}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/accounts}}}}.msg
from = "${extract{1}{:}{${lookup{$local_part}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/accounts}}}}@${lookup{$domain}dsearch{/etc/exim4/domains/}}"
headers = Content-Type: text/plain; charset=utf-8;\nContent-Transfer-Encoding: 8bit
subject = "${if def:h_Subject: {Autoreply: \"${rfc2047:$h_Subject:}\"} {Autoreply Message}}"
to = "${sender_address}"
devnull:
driver = appendfile
file = /dev/null
######################################################################
# RETRY CONFIGURATION #
######################################################################
begin retry
# Address or Domain Error Retries
# ----------------- ----- -------
* * F,2h,15m; G,16h,1h,1.5; F,4d,6h
######################################################################
# REWRITE CONFIGURATION #
######################################################################
begin rewrite
######################################################################

View File

@@ -1,512 +0,0 @@
######################################################################
# #
# Exim configuration file for Hestia Control Panel #
# #
######################################################################
#SPAMASSASSIN = yes
#SPAM_SCORE = 50
#SPAM_REJECT_SCORE = 100
#CLAMD = yes
smtp_banner = $smtp_active_hostname
smtp_active_hostname = ${lookup dnsdb{>: defer_never,ptr=$interface_address}{${listextract{1}{$value}}}{$primary_hostname}}
add_environment = <; PATH=/bin:/usr/bin
keep_environment =
disable_ipv6 = true
SRS_SECRET = ${readfile{/etc/exim4/srs.conf}}
smtputf8_advertise_hosts =
domainlist local_domains = dsearch;/etc/exim4/domains/
domainlist relay_to_domains = dsearch;/etc/exim4/domains/
hostlist relay_from_hosts = 127.0.0.1
hostlist whitelist = net-iplsearch;/etc/exim4/white-blocks.conf
hostlist spammers = net-iplsearch;/etc/exim4/spam-blocks.conf
no_local_from_check
untrusted_set_sender = *
acl_smtp_connect = acl_check_spammers
acl_smtp_mail = acl_check_mail
acl_smtp_rcpt = acl_check_rcpt
acl_smtp_data = acl_check_data
acl_smtp_mime = acl_check_mime
.ifdef SPAMASSASSIN
spamd_address = 127.0.0.1 783
.endif
.ifdef CLAMD
av_scanner = clamd: /run/clamav/clamd.ctl
.endif
log_selector = +tls_sni
tls_advertise_hosts = *
# We test that $tls_in_sni is a valid domain, by an arbitrary email address foo@domain.tld .
# Then, we extract the domain with a function that would fail if the email address is invalid.
# If the certificate exists, we will use it, otherwise the default certificate in /etc/ssl will be used.
tls_certificate = \
${if and {\
{ eq {${domain:foo@$tls_in_sni}} {$tls_in_sni}}\
{ exists{/usr/local/hestia/ssl/mail/$tls_in_sni.crt} }\
}\
{/usr/local/hestia/ssl/mail/$tls_in_sni.crt}\
{/usr/local/hestia/ssl/certificate.crt}\
}
tls_privatekey = \
${if and {\
{ eq {${domain:foo@$tls_in_sni}} {$tls_in_sni}}\
{ exists{/usr/local/hestia/ssl/mail/$tls_in_sni.key} }\
}\
{/usr/local/hestia/ssl/mail/$tls_in_sni.key}\
{/usr/local/hestia/ssl/certificate.key}\
}
daemon_smtp_ports = 25 : 465 : 587
tls_on_connect_ports = 465
tls_require_ciphers = PERFORMANCE:-RSA:-VERS-ALL:+VERS-TLS1.2:+VERS-TLS1.3:%SERVER_PRECEDENCE
never_users = root
host_lookup = *
rfc1413_hosts = *
rfc1413_query_timeout = 0s
ignore_bounce_errors_after = 2d
timeout_frozen_after = 7d
DKIM_DOMAIN = ${lc:${domain:$h_from:}}
DKIM_FILE = /etc/exim4/domains/${lookup{${lc:${domain:$h_from:}}}dsearch{/etc/exim4/domains/}}/dkim.pem
DKIM_PRIVATE_KEY = ${if exists{DKIM_FILE}{DKIM_FILE}{0}}
OUTGOING_IP = /etc/exim4/domains/${lookup{$sender_address_domain}dsearch{/etc/exim4/domains}}/ip
SMTP_RELAY_FILE = ${if exists{/etc/exim4/domains/${lookup{$sender_address_domain}dsearch{/etc/exim4/domains}}/smtp_relay.conf}{/etc/exim4/domains/${lookup{$sender_address_domain}dsearch{/etc/exim4/domains}}/smtp_relay.conf}{/etc/exim4/smtp_relay.conf}}
SMTP_RELAY_HOST = ${lookup{host}lsearch{SMTP_RELAY_FILE}}
SMTP_RELAY_PORT = ${lookup{port}lsearch{SMTP_RELAY_FILE}}
SMTP_RELAY_USER = ${lookup{user}lsearch{SMTP_RELAY_FILE}}
SMTP_RELAY_PASS = ${lookup{pass}lsearch{SMTP_RELAY_FILE}}
# Custom Filter
system_filter = /etc/exim4/system.filter
system_filter_user = Debian-exim
######################################################################
# ACL CONFIGURATION #
# Specifies access control lists for incoming SMTP mail #
######################################################################
acl_not_smtp = acl_not_smtp
begin acl
# Limit per user for PHP scripts
acl_not_smtp:
deny message = Website of user $authenticated_id is sending too many emails - rate overlimit = $sender_rate / $sender_rate_period
ratelimit = 200 / 1h / $authenticated_id
warn ratelimit = 100 / 1h / strict / $authenticated_id
log_message = Sender rate [limitlog]: log / account / $authenticated_id / $sender_rate / $sender_rate_period
accept
acl_check_spammers:
accept hosts = +whitelist
drop message = Your host in blacklist on this server.
log_message = Host in blacklist
hosts = +spammers
accept
acl_check_mail:
deny condition = ${if eq{$sender_helo_name}{}}
message = HELO required before MAIL
drop !authenticated = *
message = Helo name contains an IP address (HELO was $sender_helo_name) and not is valid
condition = ${if match{$sender_helo_name}{\N((\d{1,3}[.-]\d{1,3}[.-]\d{1,3}[.-]\d{1,3})|([0-9a-f]{8})|([0-9A-F]{8}))\N}{yes}{no}}
condition = ${if match {${lookup dnsdb{>: defer_never,ptr=$sender_host_address}}\}{$sender_helo_name}{no}{yes}}
delay = 45s
drop !authenticated = *
condition = ${if isip{$sender_helo_name}}
message = Access denied - Invalid HELO name (See RFC2821 4.1.3)
drop !authenticated = *
condition = ${if eq{[$interface_address]}{$sender_helo_name}}
message = $interface_address is _my_ address
accept
acl_check_rcpt:
accept hosts = :
# Limit per email account for SMTP auhenticated users
deny message = Email account $authenticated_id is sending too many emails - rate overlimit = $sender_rate / $sender_rate_period
set acl_c_msg_limit = ${if exists{/etc/exim4/domains/${lookup{${domain:$authenticated_id}}dsearch{/etc/exim4/domains/}}/limits}{${lookup {$authenticated_id} lsearch{/etc/exim4/domains/${lookup{${domain:$authenticated_id}}dsearch{/etc/exim4/domains/}}/limits}{$value}{${readfile{/etc/exim4/limit.conf}}}}}{${readfile{/etc/exim4/limit.conf}}} }
ratelimit = $acl_c_msg_limit / 1h / strict/ $authenticated_id
warn ratelimit = ${eval:$acl_c_msg_limit / 2} / 1h / strict / $authenticated_id
log_message = Sender rate [limitlog]: log / email / $authenticated_id / $sender_rate / $sender_rate_period
deny message = Restricted characters in address
domains = +local_domains
local_parts = ^[.] : ^.*[@%!/|]
deny message = Restricted characters in address
domains = !+local_domains
local_parts = ^[./|] : ^.*[@%!] : ^.*/\\.\\./
require verify = sender
accept hosts = +relay_from_hosts
control = submission
accept authenticated = *
control = submission/domain=
deny message = Rejected because $sender_host_address is in a black list at $dnslist_domain\n$dnslist_text
hosts = !+whitelist
dnslists = ${readfile {/etc/exim4/dnsbl.conf}{:}}
require message = relay not permitted
domains = +local_domains : +relay_to_domains
deny message = smtp auth required
sender_domains = +local_domains
!authenticated = *
require verify = recipient
.ifdef CLAMD
warn set acl_m0 = no
warn condition = ${if exists {/etc/exim4/domains/$domain/antivirus}{yes}{no}}
set acl_m0 = yes
.endif
.ifdef SPAMASSASSIN
warn set acl_m1 = no
set acl_m3 = no
warn condition = ${if exists {/etc/exim4/domains/$domain/antispam}{yes}{no}}
set acl_m1 = yes
warn condition = ${if exists {/etc/exim4/domains/$domain/reject_spam}{yes}{no}}
set acl_m3 = yes
.endif
accept
acl_check_data:
.ifdef CLAMD
deny message = Message contains a virus ($malware_name) and has been rejected
malware = */defer_ok
condition = ${if eq{$acl_m0}{yes}{yes}{no}}
.endif
.ifdef SPAMASSASSIN
warn !authenticated = *
hosts = !+relay_from_hosts
condition = ${if < {$message_size}{1024K}}
condition = ${if eq{$acl_m1}{yes}{yes}{no}}
spam = debian-spamd:true/defer_ok
add_header = X-Spam-Score: $spam_score_int
add_header = X-Spam-Bar: $spam_bar
add_header = X-Spam-Report: $spam_report
set acl_m2 = $spam_score_int
warn condition = ${if !eq{$acl_m2}{} {yes}{no}}
condition = ${if >{$acl_m2}{SPAM_SCORE} {yes}{no}}
add_header = X-Spam-Status: Yes
message = SpamAssassin detected spam (from $sender_address to $recipients).
# Deny spam at high score if spam score > SPAM_REJECT_SCORE and delete_spam is enabled
deny message = This message scored $spam_score spam points
spam = debian-spamd:true
condition = ${if eq{$acl_m3}{yes}{yes}{no}}
condition = ${if >{$spam_score_int}{SPAM_REJECT_SCORE}{1}{0}}
.endif
accept
acl_check_mime:
deny message = Blacklisted file extension detected
condition = ${if match {${lc:$mime_filename}}{\N(\.ace|\.ade|\.adp|\.app|\.arj|\.asp|\.aspx|\.asx|\.bas|\.bat|\.cab|\.cer|\.chm|\.cmd|\.cnt|\.com|\.cpl|\.crt|\.csh|\.der|\.diagcab|\.dll|\.efi|\.exe|\.fla|\.fon|\.fxp|\.gadget|\.grp|\.hlp|\.hpj|\.hta|\.htc|\.img|\.inf|\.ins|\.iso|\.isp|\.its|\.jar|\.jnlp|\.js|\.jse|\.ksh|\.lib|\.lnk|\.mad|\.maf|\.mag|\.mam|\.maq|\.mar|\.mas|\.mat|\.mau|\.mav|\.maw|\.mcf|\.mda|\.mdb|\.mde|\.mdt|\.mdw|\.mdz|\.msc|\.msh|\.msh1|\.msh1xml|\.msh2|\.msh2xml|\.mshxml|\.msi|\.msp|\.mst|\.msu|\.ops|\.osd|\.pcd|\.pif|\.pl|\.plg|\.prf|\.prg|\.printerexport|\.ps1|\.ps1xml|\.ps2|\.ps2xml|\.psc1|\.psc2|\.psd1|\.psdm1|\.pst|\.py|\.pyc|\.pyo|\.pyw|\.pyz|\.pyzw|\.reg|\.scf|\.scr|\.sct|\.sfx|\.shb|\.shs|\.swf|\.sys|\.theme|\.tmp|\.ttf|\.url|\.vb|\.vba|\.vbe|\.vbp|\.vbs|\.vhd|\.vhdx|\.vsmacros|\.vsw|\.vxd|\.webpnp|\.website|\.wim|\.ws|\.wsc|\.wsf|\.wsh|\.xbap|\.xll|\.xnk)$\N}{1}{0}}
accept
######################################################################
# AUTHENTICATION CONFIGURATION #
######################################################################
begin authenticators
smtp_relay_login:
driver = plaintext
public_name = LOGIN
hide client_send = : SMTP_RELAY_USER : SMTP_RELAY_PASS
dovecot_plain:
driver = dovecot
public_name = PLAIN
server_socket = /run/dovecot/auth-client
server_set_id = $auth1
dovecot_login:
driver = dovecot
public_name = LOGIN
server_socket = /run/dovecot/auth-client
server_set_id = $auth1
######################################################################
# ROUTERS CONFIGURATION #
# Specifies how addresses are handled #
######################################################################
begin routers
send_via_unauthenticated_smtp_relay:
driver = manualroute
address_data = SMTP_RELAY_HOST:SMTP_RELAY_PORT
domains = !+local_domains
require_files = SMTP_RELAY_FILE
condition = ${if eq{SMTP_RELAY_USER}{}}
transport = remote_smtp
route_list = * ${extract{1}{:}{$address_data}}::${extract{2}{:}{$address_data}}
no_more
no_verify
send_via_smtp_relay:
driver = manualroute
address_data = SMTP_RELAY_HOST:SMTP_RELAY_PORT
domains = !+local_domains
require_files = SMTP_RELAY_FILE
transport = smtp_relay_smtp
route_list = * ${extract{1}{:}{$address_data}}::${extract{2}{:}{$address_data}}
no_more
no_verify
dnslookup:
driver = dnslookup
# if outbound, and forwarding has been done, use an alternate transport
domains = ! +local_domains
transport = ${if eq {$local_part@$domain} \
{$original_local_part@$original_domain} \
{remote_smtp} {remote_forwarded_smtp}}
no_more
userforward:
driver = redirect
check_local_user
file = $home/.forward
require_files = ${local_part}:+${home}/.forward
domains = +local_domains
allow_filter
no_verify
no_expn
check_ancestor
file_transport = address_file
pipe_transport = address_pipe
reply_transport = address_reply
procmail:
driver = accept
check_local_user
require_files = ${local_part}:+${home}/.procmailrc:/usr/bin/procmail
transport = procmail
no_verify
autoreplay:
driver = accept
require_files = /etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/autoreply.${local_part}.msg
condition = ${if exists{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/autoreply.${local_part}.msg}{yes}{no}}
retry_use_local_part
transport = userautoreply
unseen
inbound_srs:
driver = redirect
senders = :
domains = +local_domains
# detect inbound bounces which are SRS'd, and decode them
condition = ${if inbound_srs {$local_part} {SRS_SECRET}}
data = $srs_recipient
inbound_srs_failure:
driver = redirect
senders = :
domains = +local_domains
# detect inbound bounces which look SRS'd but are invalid
condition = ${if inbound_srs {$local_part} {}}
allow_fail
data = :fail: Invalid SRS recipient address
aliases:
driver = redirect
headers_add = X-redirected: yes
data = ${extract{1}{:}{${lookup{$local_part@$domain}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/aliases}}}}
require_files = /etc/exim4/domains/$domain/aliases
redirect_router = dnslookup
pipe_transport = address_pipe
unseen
localuser_fwd_only:
driver = accept
transport = devnull
condition = ${if exists{/etc/exim4/domains/$domain/fwd_only}{${lookup{$local_part}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/fwd_only}{true}{false}}}}
localuser_spam:
driver = accept
transport = local_spam_delivery
condition = ${if eq {${if match{$h_X-Spam-Status:}{\N^Yes\N}{yes}{no}}} {${lookup{$local_part}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/passwd}{yes}{no_such_user}}}}
localuser:
driver = accept
transport = local_delivery
condition = ${lookup{$local_part}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/passwd}{true}{false}}
catchall:
driver = redirect
headers_add = X-redirected: yes
require_files = /etc/exim4/domains/$domain/aliases
data = ${extract{1}{:}{${lookup{*@$domain}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/aliases}}}}
file_transport = local_delivery
redirect_router = dnslookup
condition = ${lookup{$local_part@$domain}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/aliases}{false}{true}}
terminate_alias:
driver = accept
transport = devnull
condition = ${lookup{$local_part@$domain}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/aliases}{true}{false}}
######################################################################
# TRANSPORTS CONFIGURATION #
######################################################################
begin transports
smtp_relay_smtp:
driver = smtp
hosts_require_auth = $host_address
hosts_require_tls = $host_address
remote_smtp:
driver = smtp
helo_data = ${lookup dnsdb{>: defer_never,ptr=$sending_ip_address}{${listextract{1}{$value}}}{$primary_hostname}}
dkim_domain = DKIM_DOMAIN
dkim_selector = mail
dkim_private_key = DKIM_PRIVATE_KEY
dkim_canon = relaxed
dkim_strict = 0
hosts_try_fastopen = !*.l.google.com
interface = ${if exists{OUTGOING_IP}{${readfile{OUTGOING_IP}}}}
remote_forwarded_smtp:
driver = smtp
helo_data = ${lookup dnsdb{>: defer_never,ptr=$sending_ip_address}{${listextract{1}{$value}}}{$primary_hostname}}
dkim_domain = DKIM_DOMAIN
dkim_selector = mail
dkim_private_key = DKIM_PRIVATE_KEY
dkim_canon = relaxed
dkim_strict = 0
hosts_try_fastopen = !*.l.google.com
interface = ${if exists{OUTGOING_IP}{${readfile{OUTGOING_IP}}}}
# modify the envelope from, for mails that we forward
max_rcpt = 1
return_path = ${srs_encode {SRS_SECRET} {$return_path} {$original_domain}}
procmail:
driver = pipe
command = "/usr/bin/procmail -d $local_part"
return_path_add
delivery_date_add
envelope_to_add
user = $local_part
initgroups
return_output
local_delivery:
driver = appendfile
maildir_format
maildir_use_size_file
user = ${extract{2}{:}{${lookup{$local_part}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/passwd}}}}
group = mail
create_directory
directory_mode = 770
mode = 660
use_lockfile = no
delivery_date_add
envelope_to_add
return_path_add
directory = "${extract{5}{:}{${lookup{$local_part}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/passwd}}}}/mail/${lookup{$domain}dsearch{/etc/exim4/domains/}}/${lookup{$local_part}dsearch{${extract{5}{:}{${lookup{$local_part}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/passwd}}}}/mail/${lookup{$domain}dsearch{/etc/exim4/domains/}}}}"
quota = ${extract{6}{:}{${lookup{$local_part}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/passwd}}}}M
quota_warn_threshold = 75%
local_spam_delivery:
driver = appendfile
maildir_format
maildir_use_size_file
user = ${extract{2}{:}{${lookup{$local_part}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/passwd}}}}
group = mail
create_directory
directory_mode = 770
mode = 660
use_lockfile = no
delivery_date_add
envelope_to_add
return_path_add
directory = "${extract{5}{:}{${lookup{$local_part}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/passwd}}}}/mail/${lookup{$domain}dsearch{/etc/exim4/domains/}}/${lookup{$local_part}dsearch{${extract{5}{:}{${lookup{$local_part}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/passwd}}}}/mail/${lookup{$domain}dsearch{/etc/exim4/domains/}}}}/.Spam"
quota = ${extract{6}{:}{${lookup{$local_part}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/passwd}}}}M
quota_directory = "${extract{5}{:}{${lookup{$local_part}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/passwd}}}}/mail/${lookup{$domain}dsearch{/etc/exim4/domains/}}/${lookup{$local_part}dsearch{${extract{5}{:}{${lookup{$local_part}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/passwd}}}}/mail/${lookup{$domain}dsearch{/etc/exim4/domains/}}}}"
quota_warn_threshold = 75%
address_pipe:
driver = pipe
return_output
address_file:
driver = appendfile
delivery_date_add
envelope_to_add
return_path_add
address_reply:
driver = autoreply
userautoreply:
driver = autoreply
file = /etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/autoreply.${extract{1}{:}{${lookup{$local_part}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/accounts}}}}.msg
from = "${extract{1}{:}{${lookup{$local_part}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/accounts}}}}@${lookup{$domain}dsearch{/etc/exim4/domains/}}"
headers = Content-Type: text/plain; charset=utf-8;\nContent-Transfer-Encoding: 8bit
subject = "${if def:h_Subject: {Autoreply: \"${rfc2047:$h_Subject:}\"} {Autoreply Message}}"
to = "${sender_address}"
devnull:
driver = appendfile
file = /dev/null
######################################################################
# RETRY CONFIGURATION #
######################################################################
begin retry
# Address or Domain Error Retries
# ----------------- ----- -------
* * F,2h,15m; G,16h,1h,1.5; F,4d,6h
######################################################################
# REWRITE CONFIGURATION #
######################################################################
begin rewrite
######################################################################

View File

@@ -1,478 +0,0 @@
######################################################################
# #
# Exim configuration file for Hestia Control Panel #
# #
######################################################################
#SPAMASSASSIN = yes
#SPAM_SCORE = 50
#SPAM_REJECT_SCORE = 100
#CLAMD = yes
smtp_banner = $smtp_active_hostname
smtp_active_hostname = ${lookup dnsdb{>: defer_never,ptr=$interface_address}{${listextract{1}{$value}}}{$primary_hostname}}
add_environment = <; PATH=/bin:/usr/bin
keep_environment =
disable_ipv6 = true
smtputf8_advertise_hosts =
domainlist local_domains = dsearch;/etc/exim4/domains/
domainlist relay_to_domains = dsearch;/etc/exim4/domains/
hostlist relay_from_hosts = 127.0.0.1
hostlist whitelist = net-iplsearch;/etc/exim4/white-blocks.conf
hostlist spammers = net-iplsearch;/etc/exim4/spam-blocks.conf
no_local_from_check
untrusted_set_sender = *
acl_smtp_connect = acl_check_spammers
acl_smtp_mail = acl_check_mail
acl_smtp_rcpt = acl_check_rcpt
acl_smtp_data = acl_check_data
acl_smtp_mime = acl_check_mime
.ifdef SPAMASSASSIN
spamd_address = 127.0.0.1 783
.endif
.ifdef CLAMD
av_scanner = clamd: /run/clamav/clamd.ctl
.endif
log_selector = +tls_sni
tls_advertise_hosts = *
# We test that $tls_in_sni is a valid domain, by an arbitrary email address foo@domain.tld .
# Then, we extract the domain with a function that would fail if the email address is invalid.
# If the certificate exists, we will use it, otherwise the default certificate in /etc/ssl will be used.
tls_certificate = \
${if and {\
{ eq {${domain:foo@$tls_in_sni}} {$tls_in_sni}}\
{ exists{/usr/local/hestia/ssl/mail/$tls_in_sni.crt} }\
}\
{/usr/local/hestia/ssl/mail/$tls_in_sni.crt}\
{/usr/local/hestia/ssl/certificate.crt}\
}
tls_privatekey = \
${if and {\
{ eq {${domain:foo@$tls_in_sni}} {$tls_in_sni}}\
{ exists{/usr/local/hestia/ssl/mail/$tls_in_sni.key} }\
}\
{/usr/local/hestia/ssl/mail/$tls_in_sni.key}\
{/usr/local/hestia/ssl/certificate.key}\
}
daemon_smtp_ports = 25 : 465 : 587
tls_on_connect_ports = 465
tls_require_ciphers = PERFORMANCE:-RSA:-VERS-ALL:+VERS-TLS1.2:+VERS-TLS1.3:%SERVER_PRECEDENCE
never_users = root
host_lookup = *
rfc1413_hosts = *
rfc1413_query_timeout = 0s
ignore_bounce_errors_after = 2d
timeout_frozen_after = 7d
DKIM_DOMAIN = ${lc:${domain:$h_from:}}
DKIM_FILE = /etc/exim4/domains/${lc:${domain:$h_from:}}/dkim.pem
DKIM_PRIVATE_KEY = ${if exists{DKIM_FILE}{DKIM_FILE}{0}}
OUTGOING_IP = /etc/exim4/domains/$sender_address_domain/ip
SMTP_RELAY_FILE = ${if exists{/etc/exim4/domains/${sender_address_domain}/smtp_relay.conf}{/etc/exim4/domains/$sender_address_domain/smtp_relay.conf}{/etc/exim4/smtp_relay.conf}}
SMTP_RELAY_HOST = ${lookup{host}lsearch{SMTP_RELAY_FILE}}
SMTP_RELAY_PORT = ${lookup{port}lsearch{SMTP_RELAY_FILE}}
SMTP_RELAY_USER = ${lookup{user}lsearch{SMTP_RELAY_FILE}}
SMTP_RELAY_PASS = ${lookup{pass}lsearch{SMTP_RELAY_FILE}}
# Custom Filter
system_filter = /etc/exim4/system.filter
system_filter_user = Debian-exim
######################################################################
# ACL CONFIGURATION #
# Specifies access control lists for incoming SMTP mail #
######################################################################
acl_not_smtp = acl_not_smtp
begin acl
# Limit per user for PHP scripts
acl_not_smtp:
deny message = Website of user $authenticated_id is sending too many emails - rate overlimit = $sender_rate / $sender_rate_period
ratelimit = 200 / 1h / $authenticated_id
warn ratelimit = 100 / 1h / strict / $authenticated_id
log_message = Sender rate [limitlog]: log / account / $authenticated_id / $sender_rate / $sender_rate_period
accept
acl_check_spammers:
accept hosts = +whitelist
drop message = Your host in blacklist on this server.
log_message = Host in blacklist
hosts = +spammers
accept
acl_check_mail:
deny condition = ${if eq{$sender_helo_name}{}}
message = HELO required before MAIL
drop !authenticated = *
message = Helo name contains an IP address (HELO was $sender_helo_name) and not is valid
condition = ${if match{$sender_helo_name}{\N((\d{1,3}[.-]\d{1,3}[.-]\d{1,3}[.-]\d{1,3})|([0-9a-f]{8})|([0-9A-F]{8}))\N}{yes}{no}}
condition = ${if match {${lookup dnsdb{>: defer_never,ptr=$sender_host_address}}\}{$sender_helo_name}{no}{yes}}
delay = 45s
drop !authenticated = *
condition = ${if isip{$sender_helo_name}}
message = Access denied - Invalid HELO name (See RFC2821 4.1.3)
drop !authenticated = *
condition = ${if eq{[$interface_address]}{$sender_helo_name}}
message = $interface_address is _my_ address
accept
acl_check_rcpt:
accept hosts = :
# Limit per email account for SMTP auhenticated users
deny message = Email account $authenticated_id is sending too many emails - rate overlimit = $sender_rate / $sender_rate_period
set acl_c_msg_limit = ${if exists{/etc/exim4/domains/${lookup{${domain:$authenticated_id}}dsearch{/etc/exim4/domains/}}/limits}{${lookup {$authenticated_id} lsearch{/etc/exim4/domains/${lookup{${domain:$authenticated_id}}dsearch{/etc/exim4/domains/}}/limits}{$value}{${readfile{/etc/exim4/limit.conf}}}}}{${readfile{/etc/exim4/limit.conf}}} }
ratelimit = $acl_c_msg_limit / 1h / strict/ $authenticated_id
warn ratelimit = ${eval:$acl_c_msg_limit / 2} / 1h / strict / $authenticated_id
log_message = Sender rate [limitlog]: log / email / $authenticated_id / $sender_rate / $sender_rate_period
deny message = Restricted characters in address
domains = +local_domains
local_parts = ^[.] : ^.*[@%!/|]
deny message = Restricted characters in address
domains = !+local_domains
local_parts = ^[./|] : ^.*[@%!] : ^.*/\\.\\./
require verify = sender
accept hosts = +relay_from_hosts
control = submission
accept authenticated = *
control = submission/domain=
deny message = Rejected because $sender_host_address is in a black list at $dnslist_domain\n$dnslist_text
hosts = !+whitelist
dnslists = ${readfile {/etc/exim4/dnsbl.conf}{:}}
require message = relay not permitted
domains = +local_domains : +relay_to_domains
deny message = smtp auth required
sender_domains = +local_domains
!authenticated = *
require verify = recipient
.ifdef CLAMD
warn set acl_m0 = no
warn condition = ${if exists {/etc/exim4/domains/$domain/antivirus}{yes}{no}}
set acl_m0 = yes
.endif
.ifdef SPAMASSASSIN
warn set acl_m1 = no
set acl_m3 = no
warn condition = ${if exists {/etc/exim4/domains/$domain/antispam}{yes}{no}}
set acl_m1 = yes
warn condition = ${if exists {/etc/exim4/domains/$domain/reject_spam}{yes}{no}}
set acl_m3 = yes
.endif
accept
acl_check_data:
.ifdef CLAMD
deny message = Message contains a virus ($malware_name) and has been rejected
malware = */defer_ok
condition = ${if eq{$acl_m0}{yes}{yes}{no}}
.endif
.ifdef SPAMASSASSIN
warn !authenticated = *
hosts = !+relay_from_hosts
condition = ${if < {$message_size}{1024K}}
condition = ${if eq{$acl_m1}{yes}{yes}{no}}
spam = debian-spamd:true/defer_ok
add_header = X-Spam-Score: $spam_score_int
add_header = X-Spam-Bar: $spam_bar
add_header = X-Spam-Report: $spam_report
set acl_m2 = $spam_score_int
warn condition = ${if !eq{$acl_m2}{} {yes}{no}}
condition = ${if >{$acl_m2}{SPAM_SCORE} {yes}{no}}
add_header = X-Spam-Status: Yes
message = SpamAssassin detected spam (from $sender_address to $recipients).
# Deny spam at high score if spam score > SPAM_REJECT_SCORE and delete_spam is enabled
deny message = This message scored $spam_score spam points
spam = debian-spamd:true
condition = ${if eq{$acl_m3}{yes}{yes}{no}}
condition = ${if >{$spam_score_int}{SPAM_REJECT_SCORE}{1}{0}}
.endif
accept
acl_check_mime:
deny message = Blacklisted file extension detected
condition = ${if match {${lc:$mime_filename}}{\N(\.ace|\.ade|\.adp|\.app|\.arj|\.asp|\.aspx|\.asx|\.bas|\.bat|\.cab|\.cer|\.chm|\.cmd|\.cnt|\.com|\.cpl|\.crt|\.csh|\.der|\.diagcab|\.dll|\.efi|\.exe|\.fla|\.fon|\.fxp|\.gadget|\.grp|\.hlp|\.hpj|\.hta|\.htc|\.img|\.inf|\.ins|\.iso|\.isp|\.its|\.jar|\.jnlp|\.js|\.jse|\.ksh|\.lib|\.lnk|\.mad|\.maf|\.mag|\.mam|\.maq|\.mar|\.mas|\.mat|\.mau|\.mav|\.maw|\.mcf|\.mda|\.mdb|\.mde|\.mdt|\.mdw|\.mdz|\.msc|\.msh|\.msh1|\.msh1xml|\.msh2|\.msh2xml|\.mshxml|\.msi|\.msp|\.mst|\.msu|\.ops|\.osd|\.pcd|\.pif|\.pl|\.plg|\.prf|\.prg|\.printerexport|\.ps1|\.ps1xml|\.ps2|\.ps2xml|\.psc1|\.psc2|\.psd1|\.psdm1|\.pst|\.py|\.pyc|\.pyo|\.pyw|\.pyz|\.pyzw|\.reg|\.scf|\.scr|\.sct|\.sfx|\.shb|\.shs|\.swf|\.sys|\.theme|\.tmp|\.ttf|\.url|\.vb|\.vba|\.vbe|\.vbp|\.vbs|\.vhd|\.vhdx|\.vsmacros|\.vsw|\.vxd|\.webpnp|\.website|\.wim|\.ws|\.wsc|\.wsf|\.wsh|\.xbap|\.xll|\.xnk)$\N}{1}{0}}
accept
######################################################################
# AUTHENTICATION CONFIGURATION #
######################################################################
begin authenticators
smtp_relay_login:
driver = plaintext
public_name = LOGIN
hide client_send = : SMTP_RELAY_USER : SMTP_RELAY_PASS
dovecot_plain:
driver = dovecot
public_name = PLAIN
server_socket = /run/dovecot/auth-client
server_set_id = $auth1
dovecot_login:
driver = dovecot
public_name = LOGIN
server_socket = /run/dovecot/auth-client
server_set_id = $auth1
######################################################################
# ROUTERS CONFIGURATION #
# Specifies how addresses are handled #
######################################################################
begin routers
send_via_unauthenticated_smtp_relay:
driver = manualroute
address_data = SMTP_RELAY_HOST:SMTP_RELAY_PORT
domains = !+local_domains
require_files = SMTP_RELAY_FILE
condition = ${if eq{SMTP_RELAY_USER}{}}
transport = remote_smtp
route_list = * ${extract{1}{:}{$address_data}}::${extract{2}{:}{$address_data}}
no_more
no_verify
send_via_smtp_relay:
driver = manualroute
address_data = SMTP_RELAY_HOST:SMTP_RELAY_PORT
domains = !+local_domains
require_files = SMTP_RELAY_FILE
transport = smtp_relay_smtp
route_list = * ${extract{1}{:}{$address_data}}::${extract{2}{:}{$address_data}}
no_more
no_verify
dnslookup:
driver = dnslookup
domains = !+local_domains
transport = remote_smtp
no_more
userforward:
driver = redirect
check_local_user
file = $home/.forward
require_files = ${local_part}:+${home}/.forward
domains = +local_domains
allow_filter
no_verify
no_expn
check_ancestor
file_transport = address_file
pipe_transport = address_pipe
reply_transport = address_reply
procmail:
driver = accept
check_local_user
require_files = ${local_part}:+${home}/.procmailrc:/usr/bin/procmail
transport = procmail
no_verify
autoreplay:
driver = accept
require_files = /etc/exim4/domains/$domain/autoreply.${local_part}.msg
condition = ${if exists{/etc/exim4/domains/$domain/autoreply.${local_part}.msg}{yes}{no}}
retry_use_local_part
transport = userautoreply
unseen
aliases:
driver = redirect
headers_add = X-redirected: yes
data = ${extract{1}{:}{${lookup{$local_part@$domain}lsearch{/etc/exim4/domains/$domain/aliases}}}}
require_files = /etc/exim4/domains/$domain/aliases
redirect_router = dnslookup
pipe_transport = address_pipe
unseen
localuser_fwd_only:
driver = accept
transport = devnull
condition = ${if exists{/etc/exim4/domains/$domain/fwd_only}{${lookup{$local_part}lsearch{/etc/exim4/domains/$domain/fwd_only}{true}{false}}}}
localuser_spam:
driver = accept
transport = local_spam_delivery
condition = ${if eq {${if match{$h_X-Spam-Status:}{\N^Yes\N}{yes}{no}}} {${lookup{$local_part}lsearch{/etc/exim4/domains/$domain/passwd}{yes}{no_such_user}}}}
localuser:
driver = accept
transport = local_delivery
condition = ${lookup{$local_part}lsearch{/etc/exim4/domains/$domain/passwd}{true}{false}}
catchall:
driver = redirect
headers_add = X-redirected: yes
require_files = /etc/exim4/domains/$domain/aliases
data = ${extract{1}{:}{${lookup{*@$domain}lsearch{/etc/exim4/domains/$domain/aliases}}}}
file_transport = local_delivery
redirect_router = dnslookup
condition = ${lookup{$local_part@$domain}lsearch{/etc/exim4/domains/${lookup{$domain}dsearch{/etc/exim4/domains/}}/aliases}{false}{true}}
terminate_alias:
driver = accept
transport = devnull
condition = ${lookup{$local_part@$domain}lsearch{/etc/exim4/domains/$domain/aliases}{true}{false}}
######################################################################
# TRANSPORTS CONFIGURATION #
######################################################################
begin transports
smtp_relay_smtp:
driver = smtp
hosts_require_auth = $host_address
hosts_require_tls = $host_address
remote_smtp:
driver = smtp
helo_data = ${lookup dnsdb{>: defer_never,ptr=$sending_ip_address}{${listextract{1}{$value}}}{$primary_hostname}}
dkim_domain = DKIM_DOMAIN
dkim_selector = mail
dkim_private_key = DKIM_PRIVATE_KEY
dkim_canon = relaxed
dkim_strict = 0
hosts_try_fastopen = !*.l.google.com
interface = ${if exists{OUTGOING_IP}{${readfile{OUTGOING_IP}}}}
procmail:
driver = pipe
command = "/usr/bin/procmail -d $local_part"
return_path_add
delivery_date_add
envelope_to_add
user = $local_part
initgroups
return_output
local_delivery:
driver = appendfile
maildir_format
maildir_use_size_file
user = ${extract{2}{:}{${lookup{$local_part}lsearch{/etc/exim4/domains/$domain/passwd}}}}
group = mail
create_directory
directory_mode = 770
mode = 660
use_lockfile = no
delivery_date_add
envelope_to_add
return_path_add
directory = "${extract{5}{:}{${lookup{$local_part}lsearch{/etc/exim4/domains/$domain/passwd}}}}/mail/$domain/$local_part"
quota = ${extract{6}{:}{${lookup{$local_part}lsearch{/etc/exim4/domains/$domain/passwd}}}}M
quota_warn_threshold = 75%
local_spam_delivery:
driver = appendfile
maildir_format
maildir_use_size_file
user = ${extract{2}{:}{${lookup{$local_part}lsearch{/etc/exim4/domains/$domain/passwd}}}}
group = mail
create_directory
directory_mode = 770
mode = 660
use_lockfile = no
delivery_date_add
envelope_to_add
return_path_add
directory = "${extract{5}{:}{${lookup{$local_part}lsearch{/etc/exim4/domains/$domain/passwd}}}}/mail/$domain/$local_part/.Spam"
quota = ${extract{6}{:}{${lookup{$local_part}lsearch{/etc/exim4/domains/$domain/passwd}}}}M
quota_directory = "${extract{5}{:}{${lookup{$local_part}lsearch{/etc/exim4/domains/$domain/passwd}}}}/mail/$domain/$local_part"
quota_warn_threshold = 75%
address_pipe:
driver = pipe
return_output
address_file:
driver = appendfile
delivery_date_add
envelope_to_add
return_path_add
address_reply:
driver = autoreply
userautoreply:
driver = autoreply
file = /etc/exim4/domains/$domain/autoreply.${local_part}.msg
from = "${local_part}@${domain}"
headers = Content-Type: text/plain; charset=utf-8;\nContent-Transfer-Encoding: 8bit
subject = "${if def:h_Subject: {Autoreply: \"${rfc2047:$h_Subject:}\"} {Autoreply Message}}"
to = "${sender_address}"
devnull:
driver = appendfile
file = /dev/null
######################################################################
# RETRY CONFIGURATION #
######################################################################
begin retry
# Address or Domain Error Retries
# ----------------- ----- -------
* * F,2h,15m; G,16h,1h,1.5; F,4d,6h
######################################################################
# REWRITE CONFIGURATION #
######################################################################
begin rewrite
######################################################################

View File

@@ -1 +0,0 @@
200

View File

@@ -1 +0,0 @@
TOBEREPLACED

View File

@@ -1,16 +0,0 @@
if $h_X-Spam-Status: contains "Yes"
then
headers add "Old-Subject: $h_subject"
headers remove "Subject"
headers add "Subject: *** SPAM *** $h_old-subject"
headers remove "Old-Subject"
endif
# X-Anti-Virus: infected
if $h_X-Anti-Virus: contains "infected"
then
headers add "Old-Subject: $h_subject"
headers remove "Subject"
headers add "Subject: *** VIRUS *** $h_old-subject"
headers remove "Old-Subject"
endif

View File

@@ -1,9 +0,0 @@
# Fail2Ban configuration file for hestia
[Definition]
actionstart = /usr/local/hestia/bin/v-add-firewall-chain <name>
actionstop = /usr/local/hestia/bin/v-delete-firewall-chain <name>
actioncheck = iptables -n -L INPUT | grep -q 'fail2ban-<name>[ \t]'
actionban = /usr/local/hestia/bin/v-add-firewall-ban <ip> <name>
actionunban = /usr/local/hestia/bin/v-delete-firewall-ban <ip> <name>

View File

@@ -1,10 +0,0 @@
# Fail2Ban filter for unsuccessful hestia authentication attempts
#
[INCLUDES]
before = common.conf
[Definition]
failregex = .* <HOST> failed to login
ignoreregex =

View File

@@ -1,66 +0,0 @@
[ssh-iptables]
enabled = true
filter = sshd
action = hestia[name=SSH]
logpath = /var/log/auth.log
maxretry = 5
[vsftpd-iptables]
enabled = false
filter = vsftpd
action = hestia[name=FTP]
logpath = /var/log/vsftpd.log
maxretry = 5
[exim-iptables]
enabled = true
filter = exim
action = hestia[name=MAIL]
logpath = /var/log/exim4/mainlog
[dovecot-iptables]
enabled = true
filter = dovecot
action = hestia[name=MAIL]
logpath = /var/log/dovecot.log
[mysqld-iptables]
enabled = false
filter = mysqld-auth
action = hestia[name=DB]
logpath = /var/log/mysql/error.log
maxretry = 5
[hestia-iptables]
enabled = true
filter = hestia
action = hestia[name=HESTIA]
logpath = /var/log/hestia/auth.log
maxretry = 5
[roundcube-auth]
enabled = false
filter = roundcube-auth
action = hestia[name=WEB]
logpath = /var/log/roundcube/errors.log
maxretry = 5
[phpmyadmin-auth]
enabled = true
filter = phpmyadmin-syslog
action = hestia[name=WEB]
logpath = /var/log/auth.log
maxretry = 5
[recidive]
enabled = true
filter = recidive
action = hestia[name=RECIDIVE]
logpath = /var/log/fail2ban.log
maxretry = 5
findtime = 86400
bantime = 864000
#Uncomment and add your IPs and or domains to the Whitelist
#[DEFAULT]
#ignoreip = 111.111.111.111 222.222.222.222 subdomain.example.tld example.tld 333.333.333.333

View File

@@ -1,48 +0,0 @@
<?php
namespace Filegator\Services\Archiver\Adapters;
use Filegator\Container\Container;
use Filegator\Services\Archiver\ArchiverInterface;
use Filegator\Services\Service;
use Filegator\Services\Storage\Filesystem as Storage;
use Filegator\Services\Tmpfs\TmpfsInterface;
use function Hestiacp\quoteshellarg\quoteshellarg;
class HestiaZipArchiver extends ZipArchiver implements Service, ArchiverInterface {
protected $container;
public function __construct(TmpfsInterface $tmpfs, Container $container) {
$this->tmpfs = $tmpfs;
$this->container = $container;
}
public function uncompress(string $source, string $destination, Storage $storage) {
$auth = $this->container->get("Filegator\Services\Auth\AuthInterface");
$v_user = basename($auth->user()->getUsername());
if (!strlen($v_user)) {
return;
}
if (strpos($source, "/home") === false) {
$source = "/home/$v_user/" . $source;
}
if (strpos($destination, "/home") === false) {
$destination = "/home/$v_user/" . $destination;
}
exec(
"sudo /usr/local/hestia/bin/v-extract-fs-archive " .
quoteshellarg($v_user) .
" " .
quoteshellarg($source) .
" " .
quoteshellarg($destination),
$output,
$return_var,
);
}
}

View File

@@ -1,122 +0,0 @@
<?php
/*
* This file is part of the FileGator package.
*
* (c) Milos Stojanovic <alcalbg@gmail.com>
*
* For the full copyright and license information, please view the LICENSE file
*/
namespace Filegator\Services\Auth\Adapters;
use Filegator\Services\Auth\AuthInterface;
use Filegator\Services\Auth\User;
use Filegator\Services\Auth\UsersCollection;
use Filegator\Services\Service;
use function Hestiacp\quoteshellarg\quoteshellarg;
/**
* @codeCoverageIgnore
*/
class HestiaAuth implements Service, AuthInterface {
protected $permissions = [];
protected $private_repos = false;
protected $hestia_user = "";
public function init(array $config = []) {
if (isset($_SESSION["user"])) {
$v_user = $_SESSION["user"];
}
if (!empty($_SESSION["look"])) {
if (isset($_SESSION["look"]) && $_SESSION["userContext"] === "admin") {
$v_user = $_SESSION["look"];
}
if (
$_SESSION["look"] == "admin" &&
$_SESSION["POLICY_SYSTEM_PROTECTED_ADMIN"] == "yes"
) {
// Go away do not login
header("Location: /");
exit();
}
}
$this->hestia_user = $v_user;
$this->permissions = isset($config["permissions"]) ? (array) $config["permissions"] : [];
$this->private_repos = isset($config["private_repos"])
? (bool) $config["private_repos"]
: false;
}
public function user(): ?User {
$cmd = "/usr/bin/sudo /usr/local/hestia/bin/v-list-user";
exec($cmd . " " . quoteshellarg($this->hestia_user) . " json", $output, $return_var);
if ($return_var == 0) {
$data = json_decode(implode("", $output), true);
$hestia_user_info = $data[$this->hestia_user];
return $this->transformUser($hestia_user_info);
}
return $this->getGuest();
}
public function transformUser($hstuser): User {
$user = new User();
$user->setUsername($this->hestia_user);
$user->setName($this->hestia_user . " (" . $hstuser["NAME"] . ")");
$user->setRole("user");
$user->setPermissions($this->permissions);
$user->setHomedir("/");
return $user;
}
public function authenticate($username, $password): bool {
# Auth is handled by Hestia
return false;
}
public function forget() {
// Logout return to Hestia
return $this->getGuest();
}
public function store(User $user) {
return null; // not used
}
public function update($username, User $user, $password = ""): User {
// Password change is handled by Hestia
return $this->user();
}
public function add(User $user, $password): User {
return new User(); // not used
}
public function delete(User $user) {
return true; // not used
}
public function find($username): ?User {
return null; // not used
}
public function allUsers(): UsersCollection {
return new UsersCollection(); // not used
}
public function getGuest(): User {
$guest = new User();
$guest->setUsername("guest");
$guest->setName("Guest");
$guest->setRole("guest");
$guest->setHomedir("/");
$guest->setPermissions([]);
return $guest;
}
}

View File

@@ -1,64 +0,0 @@
<?php
/*
* This file is part of the FileGator package.
*
* (c) Milos Stojanovic <alcalbg@gmail.com>
*
* For the full copyright and license information, please view the LICENSE file
*/
namespace Filegator\Services\Session\Adapters;
use Filegator\Kernel\Request;
use Filegator\Services\Service;
use Filegator\Services\Session\Session;
use Filegator\Services\Session\SessionStorageInterface;
class SessionStorage implements Service, SessionStorageInterface {
protected $request;
protected $config;
public function __construct(Request $request) {
$this->request = $request;
}
public function init(array $config = []) {
// we don't have a previous session attached
if (!$this->getSession()) {
$handler = $config["handler"];
$session = new Session($handler());
//$session->setName('filegator');
$this->setSession($session);
}
}
public function save() {
$this->getSession()->save();
}
public function set(string $key, $data) {
return $this->getSession()->set($key, $data);
}
public function get(string $key, $default = null) {
return $this->getSession() ? $this->getSession()->get($key, $default) : $default;
}
public function invalidate() {
if (!$this->getSession()->isStarted()) {
$this->getSession()->start();
}
$this->getSession()->invalidate();
}
private function setSession(Session $session) {
return $this->request->setSession($session);
}
private function getSession(): ?Session {
return $this->request->getSession();
}
}

View File

@@ -1,47 +0,0 @@
{
"name": "filegator/filegator",
"description": "Filegator",
"license": "MIT",
"type": "project",
"config": {
"platform": {
"php": "7.2.5"
}
},
"require": {
"php": "^7.2",
"monolog/monolog": "^1.24",
"nikic/fast-route": "^1.3",
"symfony/security-csrf": "^4.4",
"symfony/http-foundation": "^4.4",
"dibi/dibi": "^4.1",
"php-di/php-di": "^6.0",
"rakit/validation": "^1.1",
"league/flysystem": "^1.1",
"league/flysystem-ziparchive": "^1.0",
"league/flysystem-sftp": "^1.0",
"hestiacp/phpquoteshellarg": "^1.0"
},
"authors": [
{
"name": "Milos Stojanovic",
"email": "alcalbg@gmail.com"
}
],
"autoload": {
"psr-4": {
"Filegator\\": "backend"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/backend/"
}
},
"require-dev": {
"phpunit/phpunit": "^8.0",
"symfony/var-dumper": "^4.4",
"league/flysystem-memory": "^1.0",
"phpstan/phpstan": "^0.11.8"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,175 +0,0 @@
<?php
use function Hestiacp\quoteshellarg\quoteshellarg;
$dist_config = require __DIR__ . "/configuration_sample.php";
$dist_config["public_path"] = "/fm/";
$dist_config["frontend_config"]["app_name"] = "File Manager - Hestia Control Panel";
$dist_config["frontend_config"]["logo"] = "../images/logo.svg";
$dist_config["frontend_config"]["editable"] = [
".txt",
".css",
".js",
".ts",
".html",
".php",
".py",
".yml",
".xml",
".md",
".log",
".csv",
".conf",
".config",
".ini",
".scss",
".sh",
".env",
".example",
".htaccess",
".twig",
".tpl",
".yaml",
];
$dist_config["frontend_config"]["guest_redirection"] = "/login/";
$dist_config["frontend_config"]["upload_max_size"] = 1024 * 1024 * 1024;
$dist_config["services"]["Filegator\Services\Storage\Filesystem"]["config"][
"adapter"
] = function () {
if (!empty($_SESSION["INACTIVE_SESSION_TIMEOUT"])) {
if ($_SESSION["INACTIVE_SESSION_TIMEOUT"] * 60 + $_SESSION["LAST_ACTIVITY"] < time()) {
$v_user = quoteshellarg($_SESSION["user"]);
$v_session_id = quoteshellarg($_SESSION["token"]);
exec(
"/usr/local/hestia/bin/v-log-user-logout " . $v_user . " " . $v_session_id,
$output,
$return_var,
);
unset($_SESSION);
session_unset();
session_destroy();
session_start();
echo '<meta http-equiv="refresh" content="0; url=/">';
exit();
} else {
$_SESSION["LAST_ACTIVITY"] = time();
}
} else {
echo '<meta http-equiv="refresh" content="0; url=/">';
}
if (isset($_SESSION["user"])) {
$v_user = $_SESSION["user"];
}
if (!empty($_SESSION["look"])) {
if (isset($_SESSION["look"]) && $_SESSION["userContext"] === "admin") {
$v_user = $_SESSION["look"];
}
if (
isset($_SESSION["look"]) &&
$_SESSION["look"] == "admin" &&
$_SESSION["POLICY_SYSTEM_PROTECTED_ADMIN"] == "yes"
) {
header("Location: /");
}
}
# Create filemanager sftp key if missing and trash it after 30 min
if (!file_exists("/home/" . basename($v_user) . "/.ssh/hst-filemanager-key")) {
exec(
"sudo /usr/local/hestia/bin/v-add-user-sftp-key " .
quoteshellarg(basename($v_user)) .
" 30",
$output,
$return_var,
);
// filemanager also requires .ssh chmod o+x ... hopefully we can improve it to g+x or u+x someday
// current minimum for filemanager: chmod 0701 .ssh
shell_exec("sudo chmod o+x " . quoteshellarg("/home/" . basename($v_user) . "/.ssh"));
}
if (!isset($_SESSION["SFTP_PORT"])) {
exec("sudo /usr/local/hestia/bin/v-list-sys-sshd-port json", $output, $result);
$port = json_decode(implode("", $output));
if (is_numeric($port[0]) && $port[0] > 0) {
$_SESSION["SFTP_PORT"] = $port[0];
} elseif (
preg_match('/^\s*Port\s+(\d+)$/im', file_get_contents("/etc/ssh/sshd_config"), $matches)
) {
$_SESSION["SFTP_PORT"] = $matches[1] ?? 22;
} else {
$_SESSION["SFTP_PORT"] = 22;
}
}
preg_match(
'/(Hestia SFTP Chroot\nMatch User)(.*)/i',
file_get_contents("/etc/ssh/sshd_config"),
$matches,
);
$user_list = explode(",", $matches[2]);
if (in_array($v_user, $user_list)) {
$root = "/";
} else {
$root = "/home/" . $v_user;
}
return new \League\Flysystem\Sftp\SftpAdapter([
"host" => "127.0.0.1",
"port" => intval($_SESSION["SFTP_PORT"]),
"username" => basename($v_user),
"privateKey" => "/home/" . basename($v_user) . "/.ssh/hst-filemanager-key",
"root" => $root,
"timeout" => 10,
"directoryPerm" => 0755,
]);
};
$dist_config["services"]["Filegator\Services\Archiver\ArchiverInterface"] = [
"handler" => "\Filegator\Services\Archiver\Adapters\HestiaZipArchiver",
"config" => [],
];
$dist_config["services"]["Filegator\Services\Auth\AuthInterface"] = [
"handler" => "\Filegator\Services\Auth\Adapters\HestiaAuth",
"config" => [
"permissions" => ["read", "write", "upload", "download", "batchdownload", "zip"],
"private_repos" => false,
],
];
$dist_config["services"]["Filegator\Services\View\ViewInterface"]["config"] = [
"add_to_head" => '
<style>
.logo {
width: 46px;
}
</style>
',
"add_to_body" => '
<script>
var checkVueLoaded = setInterval(function() {
if (document.getElementsByClassName("container").length) {
clearInterval(checkVueLoaded);
var navProfile = document.getElementsByClassName("navbar-item profile")[0]; navProfile.replaceWith(navProfile.cloneNode(true))
document.getElementsByClassName("navbar-item logout")[0].text="Exit to Control Panel \u00BB";
div = document.getElementsByClassName("container")[0];
callback = function(){
if (document.getElementsByClassName("navbar-item logout")[0]){
if ( document.getElementsByClassName("navbar-item logout")[0].text != "Exit to Control Panel \u00BB" ){
var navProfile = document.getElementsByClassName("navbar-item profile")[0]; navProfile.replaceWith(navProfile.cloneNode(true))
document.getElementsByClassName("navbar-item logout")[0].text="Exit to Control Panel \u00BB";
}
}
}
config = {
childList:true,
subtree:true
}
observer = new MutationObserver(callback);
observer.observe(div,config);
}
}, 200);
</script>',
];
return $dist_config;

View File

@@ -1,71 +0,0 @@
#!/bin/bash
# Checking root permissions
if [ "x$(id -u)" != 'x0' ]; then
echo "Error: Script can be run executed only by root"
exit 10
fi
if [ -z "$HESTIA" ]; then
HESTIA="/usr/local/hestia"
fi
user='admin'
fm_error='no'
source $HESTIA/func/main.sh
source $HESTIA/install/upgrade/upgrade.conf
if [ -z "$HOMEDIR" ] || [ -z "$HESTIA_INSTALL_DIR" ]; then
echo "Error: Hestia environment vars not present"
exit 2
fi
FM_INSTALL_DIR="$HESTIA/web/fm"
FM_FILE="filegator_latest"
FM_URL="https://github.com/filegator/static/raw/master/builds/filegator_latest.zip"
COMPOSER_BIN="$HOMEDIR/$user/.composer/composer"
if [ ! -f "$COMPOSER_BIN" ]; then
$BIN/v-add-user-composer "$user"
if [ $? -ne 0 ]; then
$BIN/v-add-user-notification admin 'Composer installation failed!' '<p class="u-text-bold">The File Manager will not work without Composer.</p><p>Please try running the installer from a shell session:<br><code>bash $HESTIA/install/deb/filemanager/install-fm.sh</code></p><p>If this issue continues, please <a href="https://github.com/hestiacp/hestiacp/issues" target="_blank">open an issue on GitHub</a>.</p>'
fm_error='yes'
fi
fi
if [ "$fm_error" != "yes" ]; then
rm --recursive --force "$FM_INSTALL_DIR"
mkdir -p "$FM_INSTALL_DIR"
cd "$FM_INSTALL_DIR"
[ ! -f "${FM_INSTALL_DIR}/${FM_FILE}" ] && wget "$FM_URL" --quiet -O "${FM_INSTALL_DIR}/${FM_FILE}.zip"
unzip -qq "${FM_INSTALL_DIR}/${FM_FILE}.zip"
mv --force ${FM_INSTALL_DIR}/filegator/* "${FM_INSTALL_DIR}"
rm --recursive --force ${FM_INSTALL_DIR}/${FM_FILE}
[[ -f "${FM_INSTALL_DIR}/${FM_FILE}" ]] && rm "${FM_INSTALL_DIR}/${FM_FILE}"
cp --recursive --force ${HESTIA_INSTALL_DIR}/filemanager/filegator/* "${FM_INSTALL_DIR}"
chown $user: -R "${FM_INSTALL_DIR}"
# Check if php7.3 is available and run the installer
if [ -f "/usr/bin/php7.3" ]; then
COMPOSER_HOME="$HOMEDIR/$user/.config/composer" user_exec /usr/bin/php7.3 $COMPOSER_BIN --quiet --no-dev install
if [ $? -ne 0 ]; then
$BIN/v-add-user-notification admin 'File Manager installation failed!' '<p>Please try running the installer from a shell session:<br><code>bash $HESTIA/install/deb/filemanager/install-fm.sh</code></p><p>If this issue continues, please <a href="https://github.com/hestiacp/hestiacp/issues" target="_blank">open an issue on GitHub</a>.</p>'
fm_error="yes"
fi
else
$BIN/v-add-user-notification admin 'File Manager installation failed!' '<p class="u-text-bold">Unable to proceed with installation of File Manager.</p><p>Package <span class="u-text-bold">php7.3-cli</span> is missing from your system. Please check your PHP installation and environment settings.</p>'
fm_error="yes"
fi
if [ "$fm_error" != "yes" ]; then
chown root: -R "${FM_INSTALL_DIR}"
chown $user: "${FM_INSTALL_DIR}/private"
chown $user: "${FM_INSTALL_DIR}/private/logs"
chown $user: "${FM_INSTALL_DIR}/repository"
fi
fi

View File

@@ -1,19 +0,0 @@
/var/log/apache2/*.log /var/log/apache2/domains/*log {
rotate 4
weekly
missingok
notifempty
compress
delaycompress
create 640
sharedscripts
postrotate
/etc/init.d/apache2 reload > /dev/null || true
[ ! -f /run/nginx.pid ] || kill -USR1 `cat /run/nginx.pid`
endscript
prerotate
if [ -d /etc/logrotate.d/httpd-prerotate ]; then \
run-parts /etc/logrotate.d/httpd-prerotate; \
fi; \
endscript
}

Some files were not shown because too many files have changed in this diff Show More