diff --git a/bin/v-bunkerweb-module b/bin/v-bunkerweb-module index 80a69b7..64a452f 100755 --- a/bin/v-bunkerweb-module +++ b/bin/v-bunkerweb-module @@ -2,7 +2,7 @@ # info: action with bunkerweb API # options: COMMAND [SERVICE_NAME | SSL_CERT | SSL_KEY | FORMAT] # -# example: v-ext-modules list json +# example: v-bunkerweb-module list json # # This function enables and disables additional modules # diff --git a/bin/v-bunkerweb-module-install b/bin/v-bunkerweb-module-install new file mode 100755 index 0000000..8be357d --- /dev/null +++ b/bin/v-bunkerweb-module-install @@ -0,0 +1,460 @@ +#!/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) + +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 # +#----------------------------------------------------------# + +# 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_PASSWORD = SecureRandom.alphanumeric(24) + "!@#" +ADMIN_PASSWORD = SecureRandom.alphanumeric(24) + "!@#" + +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 "" + +# 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= + # MAX_THREADS= + + # --- 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: + # 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) +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) +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") + +# 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) +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: "/bw", + 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}/bw" + else + puts " http://#{server_ip_addr}/bw" + 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 diff --git a/func_ruby/ext-modules/bunkerweb_module.mod b/func_ruby/ext-modules/bunkerweb_module.mod index fbe4087..fcf4782 100644 --- a/func_ruby/ext-modules/bunkerweb_module.mod +++ b/func_ruby/ext-modules/bunkerweb_module.mod @@ -13,6 +13,29 @@ class BunkerwebWorker < Kernel::ModuleCoreWorker } 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 @@ -92,13 +115,57 @@ class BunkerwebWorker < Kernel::ModuleCoreWorker 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" when "help" puts "#{$0} bunkerweb_module COMMAND [OPTIONS] [json|csv|plain]" puts "COMMANDS:" - puts " add - add domain to bunkerweb" - puts " delete - delete domain from bunkerweb" - puts " addssl [path_to_cert] [path_to_key] - add existsing certificate to bunkerweb domain" - puts " updssl [path_to_cert] [path_to_key] - update existsing certificate to bunkerweb domain" + 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 diff --git a/func_ruby/ext-modules/payload/bunkerweb_module/bunkerweb_installer.yml b/func_ruby/ext-modules/payload/bunkerweb_module/bunkerweb_installer.yml new file mode 100644 index 0000000..deef6d3 --- /dev/null +++ b/func_ruby/ext-modules/payload/bunkerweb_module/bunkerweb_installer.yml @@ -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