Added API for bunkerweb
This commit is contained in:
199
bin/v-bunkerweb-module
Normal file
199
bin/v-bunkerweb-module
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
#!/opt/brepo/ruby33/bin/ruby
|
||||||
|
# info: action with bunkerweb API
|
||||||
|
# options: COMMAND [SERVICE_NAME | SSL_CERT | SSL_KEY | FORMAT]
|
||||||
|
#
|
||||||
|
# example: v-ext-modules list json
|
||||||
|
#
|
||||||
|
# This function enables and disables additional modules
|
||||||
|
#
|
||||||
|
# Commands:
|
||||||
|
# add [domain_name]
|
||||||
|
# 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_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? || existing_services.strip.empty?
|
||||||
|
result_arr = []
|
||||||
|
else
|
||||||
|
services_data = JSON.parse(existing_services)
|
||||||
|
if services_data["services"]
|
||||||
|
if services_data["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 = services_data["services"]
|
||||||
|
else
|
||||||
|
result_arr = []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
api.create_service(v_domain, {
|
||||||
|
ssl: "no",
|
||||||
|
reverse_proxy_host: "http://127.0.0.1:#{$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 :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? || existing_services.strip.empty?
|
||||||
|
result_arr = []
|
||||||
|
else
|
||||||
|
services_data = JSON.parse(existing_services)
|
||||||
|
if services_data["services"]
|
||||||
|
result_arr = services_data["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? || existing_services.strip.empty?
|
||||||
|
result_arr = []
|
||||||
|
else
|
||||||
|
services_data = JSON.parse(existing_services)
|
||||||
|
if services_data["services"]
|
||||||
|
result_arr = services_data["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 :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? || existing_services.strip.empty?
|
||||||
|
result_arr = []
|
||||||
|
else
|
||||||
|
services_data = JSON.parse(existing_services)
|
||||||
|
if services_data["services"]
|
||||||
|
result_arr = services_data["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
|
||||||
485
func_ruby/HestiaBunkerWebApi.rb
Normal file
485
func_ruby/HestiaBunkerWebApi.rb
Normal file
@@ -0,0 +1,485 @@
|
|||||||
|
#!/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",
|
||||||
|
}
|
||||||
|
|
||||||
|
# SSL configuration - only if USE_SSL != "no"
|
||||||
|
ssl_enabled = options[:ssl] && options[:ssl] != "no"
|
||||||
|
variables["USE_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["SSL_CERTIFICATE_FILE_PATH"] = options[:certificate_path]
|
||||||
|
variables["SSL_KEY_FILE_PATH"] = 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] || "~ ^/(.*)$"
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
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 current variables from the service configuration
|
||||||
|
current_vars = get_service_response[:body]["variables"] || {}
|
||||||
|
|
||||||
|
# Update SSL settings
|
||||||
|
updated_vars = {
|
||||||
|
"USE_SSL" => "yes",
|
||||||
|
"SSL_CERTIFICATE_FILE_PATH" => certificate_path,
|
||||||
|
"SSL_KEY_FILE_PATH" => 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 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
|
||||||
467
func_ruby/docs/BunkerWebApiDoc.md
Normal file
467
func_ruby/docs/BunkerWebApiDoc.md
Normal 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)
|
||||||
124
func_ruby/ext-modules/bunkerweb_module.mod
Normal file
124
func_ruby/ext-modules/bunkerweb_module.mod
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
#!/opt/brepo/ruby33/bin/ruby
|
||||||
|
|
||||||
|
class BunkerwebWorker < Kernel::ModuleCoreWorker
|
||||||
|
MODULE_ID = "bunkerweb_module"
|
||||||
|
|
||||||
|
def info
|
||||||
|
{
|
||||||
|
ID: 5,
|
||||||
|
NAME: MODULE_ID,
|
||||||
|
DESCR: "Bunkerweb enabling",
|
||||||
|
REQ: "",
|
||||||
|
CONF: "yes",
|
||||||
|
}
|
||||||
|
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?
|
||||||
|
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 add #{m_domain} 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("add 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 "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 "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 " 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
0
web/extm/bunkerweb_module/edit/index.php
Normal file
0
web/extm/bunkerweb_module/edit/index.php
Normal file
Reference in New Issue
Block a user