From 8612e71996a6f2aeeae3859de4f8c80449e6eb0c Mon Sep 17 00:00:00 2001 From: Dmitriy Fofanov Date: Tue, 28 Oct 2025 00:19:51 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE=20=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=82=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B4=D0=B0=D1=82=D1=8B?= =?UTF-8?q?=20=D0=B2=20=D0=BB=D0=BE=D0=B3=D0=B0=D1=85=20=D0=BD=D0=B0=20DD.?= =?UTF-8?q?MM.YYYY=20=D0=B4=D0=BB=D1=8F=20=D1=81=D0=BA=D1=80=D0=B8=D0=BF?= =?UTF-8?q?=D1=82=D0=BE=D0=B2=20=D0=B8=20=D1=85=D1=83=D0=BA=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 107 +++++++++++++++++++++++++++------------ gitea-hooks/post-receive | 2 +- letsencrypt_regru_api.py | 74 +++++++++++++++++---------- 3 files changed, 123 insertions(+), 60 deletions(-) diff --git a/Makefile b/Makefile index 0e333ad..795e0a8 100644 --- a/Makefile +++ b/Makefile @@ -103,11 +103,33 @@ setup-dirs: # Установка зависимостей install-dependencies: @echo "$(YELLOW)→ Установка зависимостей Python...$(NC)" - @if ! command -v pip3 >/dev/null 2>&1; then \ - echo "$(RED)✗ pip3 не найден. Установите python3-pip$(NC)"; \ + @if command -v pip3 >/dev/null 2>&1; then \ + pip3 install -q requests cryptography 2>/dev/null || pip3 install requests cryptography; \ + elif command -v pip >/dev/null 2>&1; then \ + pip install -q requests cryptography 2>/dev/null || pip install requests cryptography; \ + elif command -v python3 >/dev/null 2>&1; then \ + if python3 -m pip --version >/dev/null 2>&1; then \ + python3 -m pip install -q requests cryptography 2>/dev/null || python3 -m pip install requests cryptography; \ + else \ + echo "$(RED)✗ pip не установлен. Выполните:$(NC)"; \ + echo " $(CYAN)sudo apt-get update && sudo apt-get install -y python3-pip$(NC)"; \ + echo " или"; \ + echo " $(CYAN)curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && python3 get-pip.py$(NC)"; \ + exit 1; \ + fi; \ + elif command -v python >/dev/null 2>&1; then \ + if python -m pip --version >/dev/null 2>&1; then \ + python -m pip install -q requests cryptography 2>/dev/null || python -m pip install requests cryptography; \ + else \ + echo "$(RED)✗ pip не установлен. Выполните:$(NC)"; \ + echo " $(CYAN)sudo apt-get update && sudo apt-get install -y python3-pip$(NC)"; \ + exit 1; \ + fi; \ + else \ + echo "$(RED)✗ Python не найден. Установите Python 3:$(NC)"; \ + echo " $(CYAN)sudo apt-get update && sudo apt-get install -y python3 python3-pip$(NC)"; \ exit 1; \ fi - @pip3 install -q requests cryptography 2>/dev/null || pip3 install requests cryptography @echo "$(GREEN)✓ Зависимости установлены$(NC)" # Копирование скрипта @@ -193,17 +215,20 @@ uninstall: check-root @echo "$(RED)║ Удаление Let's Encrypt SSL Manager ║$(NC)" @echo "$(RED)╚════════════════════════════════════════════════════════════════╝$(NC)" @echo "" - @read -p "Вы уверены? Это удалит все файлы и настройки [y/N]: " -n 1 -r; \ - echo ""; \ - if [[ $$REPLY =~ ^[Yy]$$ ]]; then \ - $(MAKE) remove-service; \ - $(MAKE) remove-cron; \ - $(MAKE) remove-files; \ - echo ""; \ - echo "$(GREEN)✓ Удаление завершено$(NC)"; \ - else \ - echo "$(YELLOW)Удаление отменено$(NC)"; \ - fi + @printf "Вы уверены? Это удалит все файлы и настройки [y/N]: "; \ + read REPLY; \ + case "$$REPLY" in \ + [Yy]* ) \ + $(MAKE) remove-service; \ + $(MAKE) remove-cron; \ + $(MAKE) remove-files; \ + echo ""; \ + echo "$(GREEN)✓ Удаление завершено$(NC)"; \ + ;; \ + * ) \ + echo "$(YELLOW)Удаление отменено$(NC)"; \ + ;; \ + esac # Удаление systemd service remove-service: @@ -229,23 +254,29 @@ remove-files: @rm -rf $(INSTALL_DIR) @echo "$(GREEN)✓ Директория $(INSTALL_DIR) удалена$(NC)" @echo "" - @read -p "Удалить конфигурацию $(CONFIG_FILE)? [y/N]: " -n 1 -r; \ - echo ""; \ - if [[ $$REPLY =~ ^[Yy]$$ ]]; then \ - rm -f $(CONFIG_FILE); \ - echo "$(GREEN)✓ Конфигурация удалена$(NC)"; \ - else \ - echo "$(YELLOW)Конфигурация сохранена$(NC)"; \ - fi + @printf "Удалить конфигурацию $(CONFIG_FILE)? [y/N]: "; \ + read REPLY; \ + case "$$REPLY" in \ + [Yy]* ) \ + rm -f $(CONFIG_FILE); \ + echo "$(GREEN)✓ Конфигурация удалена$(NC)"; \ + ;; \ + * ) \ + echo "$(YELLOW)Конфигурация сохранена$(NC)"; \ + ;; \ + esac @echo "" - @read -p "Удалить логи? [y/N]: " -n 1 -r; \ - echo ""; \ - if [[ $$REPLY =~ ^[Yy]$$ ]]; then \ - rm -f $(LOG_FILE) $(CRON_LOG); \ - echo "$(GREEN)✓ Логи удалены$(NC)"; \ - else \ - echo "$(YELLOW)Логи сохранены$(NC)"; \ - fi + @printf "Удалить логи? [y/N]: "; \ + read REPLY; \ + case "$$REPLY" in \ + [Yy]* ) \ + rm -f $(LOG_FILE) $(CRON_LOG); \ + echo "$(GREEN)✓ Логи удалены$(NC)"; \ + ;; \ + * ) \ + echo "$(YELLOW)Логи сохранены$(NC)"; \ + ;; \ + esac # ============================================================================== # Утилиты @@ -301,12 +332,24 @@ check-config: @echo "" @if [ ! -f "$(CONFIG_FILE)" ]; then \ echo "$(RED)✗ Конфигурация не найдена: $(CONFIG_FILE)$(NC)"; \ + echo "$(YELLOW)Совет: Скопируйте config.json.example в $(CONFIG_FILE)$(NC)"; \ + echo " $(CYAN)sudo cp config.json.example $(CONFIG_FILE)$(NC)"; \ + echo " $(CYAN)sudo chmod 644 $(CONFIG_FILE)$(NC)"; \ exit 1; \ fi @echo "$(GREEN)✓ Конфигурация найдена$(NC)" + @if [ ! -r "$(CONFIG_FILE)" ]; then \ + echo "$(RED)✗ Нет прав для чтения: $(CONFIG_FILE)$(NC)"; \ + echo "$(YELLOW)Решение: Запустите команду с sudo:$(NC)"; \ + echo " $(CYAN)sudo make check-config$(NC)"; \ + exit 1; \ + fi @echo "" - @$(PYTHON) -c "import json; print(json.dumps(json.load(open('$(CONFIG_FILE)')), indent=2, ensure_ascii=False))" 2>/dev/null || \ - (echo "$(RED)✗ Ошибка: Неверный формат JSON$(NC)"; exit 1) + @$(PYTHON) -c "import json; print(json.dumps(json.load(open('$(CONFIG_FILE)')), indent=2, ensure_ascii=False))" 2>&1 || \ + (echo "$(RED)✗ Ошибка: Неверный формат JSON$(NC)"; \ + echo "$(YELLOW)Подробности:$(NC)"; \ + $(PYTHON) -c "import json; json.load(open('$(CONFIG_FILE)'))" 2>&1 | head -5; \ + exit 1) @echo "" @echo "$(YELLOW)→ Проверка обязательных параметров:$(NC)" @$(PYTHON) -c "import json; c=json.load(open('$(CONFIG_FILE)')); assert c.get('regru_username'), 'regru_username не задан'" && echo " $(GREEN)✓ regru_username$(NC)" || echo " $(RED)✗ regru_username$(NC)" diff --git a/gitea-hooks/post-receive b/gitea-hooks/post-receive index c13ffb1..21ce0c4 100644 --- a/gitea-hooks/post-receive +++ b/gitea-hooks/post-receive @@ -28,7 +28,7 @@ NC='\033[0m' # No Color # Функция логирования # ============================================================================== log() { - echo -e "${2:-$NC}[$(date +'%Y-%m-%d %H:%M:%S')] $1${NC}" | tee -a "$LOG_FILE" + echo -e "${2:-$NC}[$(date +'%d.%m.%Y %H:%M:%S')] $1${NC}" | tee -a "$LOG_FILE" } # ============================================================================== diff --git a/letsencrypt_regru_api.py b/letsencrypt_regru_api.py index 86763fd..984a09a 100644 --- a/letsencrypt_regru_api.py +++ b/letsencrypt_regru_api.py @@ -115,7 +115,7 @@ def setup_logging(log_file: str, verbose: bool = False) -> logging.Logger: # Настройка форматирования formatter = logging.Formatter( '%(asctime)s - %(levelname)s - %(message)s', - datefmt='%Y-%m-%d %H:%M:%S' + datefmt='%d.%m.%Y %H:%M:%S' ) # Создаем logger @@ -406,6 +406,10 @@ class NginxProxyManagerAPI: """ Загрузка нового сертификата в NPM + ВАЖНО: NPM автоматически извлекает информацию из сертификата. + Мы загружаем сертификат через веб-интерфейс формы (multipart/form-data), + а не через JSON API, так как JSON endpoint имеет строгую валидацию схемы. + Args: domain: Основной домен cert_path: Путь к файлу сертификата @@ -425,29 +429,36 @@ class NginxProxyManagerAPI: with open(key_path, 'r') as f: certificate_key = f.read() - # Если есть цепочка, объединяем с сертификатом + # Используем промежуточный сертификат если доступен + intermediate_certificate = "" if chain_path and os.path.exists(chain_path): with open(chain_path, 'r') as f: - chain = f.read() - # NPM ожидает fullchain (cert + chain) - certificate = certificate + "\n" + chain + intermediate_certificate = f.read() - # Формируем payload для API - payload = { - "provider": "other", - "nice_name": f"{domain} - Let's Encrypt", - "domain_names": [domain], - "certificate": certificate, - "certificate_key": certificate_key, - "meta": { - "letsencrypt_agree": True, - "dns_challenge": True, - "dns_provider": "reg_ru" - } + # NPM Web UI использует multipart/form-data для загрузки custom сертификатов + # Эмулируем загрузку через веб-форму + files = { + 'certificate': ('cert.pem', certificate, 'application/x-pem-file'), + 'certificate_key': ('privkey.pem', certificate_key, 'application/x-pem-file'), } + # Добавляем промежуточный сертификат если есть + if intermediate_certificate: + files['intermediate_certificate'] = ('chain.pem', intermediate_certificate, 'application/x-pem-file') + + # Дополнительные поля формы + data = { + 'nice_name': domain, + 'provider': 'other', # Обязательное поле: 'letsencrypt' или 'other' + } + + self.logger.debug(f"Uploading certificate as multipart/form-data") + self.logger.debug(f"Files: {list(files.keys())}") + self.logger.debug(f"Data: {data}") self.logger.info(f"Загрузка сертификата для {domain} в NPM...") - response = self.session.post(url, json=payload, timeout=30) + + # Отправляем как multipart/form-data + response = self.session.post(url, files=files, data=data, timeout=30) response.raise_for_status() result = response.json() @@ -493,20 +504,29 @@ class NginxProxyManagerAPI: with open(key_path, 'r') as f: certificate_key = f.read() - # Если есть цепочка, объединяем с сертификатом + # Используем промежуточный сертификат если доступен + intermediate_certificate = "" if chain_path and os.path.exists(chain_path): with open(chain_path, 'r') as f: - chain = f.read() - certificate = certificate + "\n" + chain + intermediate_certificate = f.read() - # Формируем payload для обновления - payload = { - "certificate": certificate, - "certificate_key": certificate_key + # NPM Web UI использует multipart/form-data для обновления + files = { + 'certificate': ('cert.pem', certificate, 'application/x-pem-file'), + 'certificate_key': ('privkey.pem', certificate_key, 'application/x-pem-file'), + } + + # Добавляем промежуточный сертификат если есть + if intermediate_certificate: + files['intermediate_certificate'] = ('chain.pem', intermediate_certificate, 'application/x-pem-file') + + # Дополнительные поля формы + data = { + 'provider': 'other', # Обязательное поле } self.logger.info(f"Обновление сертификата ID {cert_id} в NPM...") - response = self.session.put(url, json=payload, timeout=30) + response = self.session.put(url, files=files, data=data, timeout=30) response.raise_for_status() self.logger.info("Сертификат успешно обновлен в NPM") @@ -813,7 +833,7 @@ class LetsEncryptManager: expiry_date = cert.not_valid_after days_left = (expiry_date - datetime.now()).days - self.logger.info(f"Сертификат истекает: {expiry_date.strftime('%Y-%m-%d')}") + self.logger.info(f"Сертификат истекает: {expiry_date.strftime('%d.%m.%Y %H:%M:%S')}") self.logger.info(f"Осталось дней: {days_left}") return days_left