468 lines
15 KiB
Markdown
468 lines
15 KiB
Markdown
|
|
# 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)
|