feat(admin): загрузка дистрибутива КриптоПро через UI + активация лицензии
В карточке «СКЗИ» страницы /admin/setup добавлены два новых блока: 1. «Установка КриптоПро CSP» — multipart-форма с input type=file. Принимает .tar/.tgz/.tar.gz/.rpm (формат с cryptopro.ru). После загрузки на сервер (лимит 256 МБ): - сохраняет архив в /tmp/bj-cryptopro/ - распаковывает (tar -xzf или tar -xf) - находит все .rpm в распакованной директории - выполняет sudo rpm -Uvh --replacepkgs --nosignature на найденные пакеты - возвращает результат с количеством установленных пакетов и выводом rpm 2. «Активация лицензии» — поле для ввода серийника и кнопка. Вызывает /opt/cprocsp/sbin/amd64/cpconfig -license -set <серийник>. Если cpconfig не найден — показывает подсказку про /admin/help/cryptopro. После успеха сохраняет серийник в runtime-конфиге. internal/lkgateway/setup.go: - handler installCryptoPro (multipart form, parse, untar, find rpms, sudo rpm) - handler activateLicense (cpconfig -license -set, сохранение в RuntimeConfig) - общие хелперы runCmd / runCmdInDir для exec через context internal/lkgateway/web/templates/admin_setup.html: - секция «Установка КриптоПро CSP» с формой загрузки - секция «Активация лицензии» с полем + кнопкой - ссылки на /admin/help/cryptopro и cryptopro.ru/products/csp/downloads Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,9 +4,13 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -40,11 +44,145 @@ func registerSetup(mux *http.ServeMux, a *admin, rc *RuntimeConfig, svc *Service
|
||||
mux.HandleFunc("/admin/setup/postgres", h.savePostgres)
|
||||
mux.HandleFunc("/admin/setup/crypto", h.saveCrypto)
|
||||
mux.HandleFunc("/admin/setup/crypto/check", h.checkCrypto)
|
||||
mux.HandleFunc("/admin/setup/crypto/activate", h.activateLicense)
|
||||
mux.HandleFunc("/admin/setup/crypto/install", h.installCryptoPro)
|
||||
mux.HandleFunc("/admin/setup/nsd", h.saveNSD)
|
||||
mux.HandleFunc("/admin/setup/lk", h.saveLK)
|
||||
mux.HandleFunc("/admin/setup/test-run", h.testRun)
|
||||
}
|
||||
|
||||
// installCryptoPro — POST /admin/setup/crypto/install (multipart).
|
||||
// Принимает tar или tar.gz архив с дистрибутивом КриптоПро CSP (как
|
||||
// linux-amd64.tgz с cryptopro.ru), распаковывает в /tmp/bj-cryptopro,
|
||||
// находит все .rpm файлы и устанавливает через sudo rpm -i.
|
||||
// На РЕД ОС / ALT / ROSA это даёт рабочий /opt/cprocsp/.
|
||||
func (h *setupHandlers) installCryptoPro(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
// Архив КриптоПро ~50-100 МБ — поднимем лимит до 256 МБ.
|
||||
if err := r.ParseMultipartForm(256 << 20); err != nil {
|
||||
setupFlash(w, r, "Установка: ошибка чтения формы: "+err.Error())
|
||||
return
|
||||
}
|
||||
file, header, err := r.FormFile("dist")
|
||||
if err != nil {
|
||||
setupFlash(w, r, "Установка: выберите файл архива дистрибутива (.tar/.tgz/.tar.gz/.rpm)")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
dir := "/tmp/bj-cryptopro"
|
||||
_ = os.RemoveAll(dir)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
setupFlash(w, r, "Установка: не получилось создать "+dir+": "+err.Error())
|
||||
return
|
||||
}
|
||||
dst := filepath.Join(dir, filepath.Base(header.Filename))
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
setupFlash(w, r, "Установка: не получилось создать "+dst+": "+err.Error())
|
||||
return
|
||||
}
|
||||
if _, err := io.Copy(out, file); err != nil {
|
||||
out.Close()
|
||||
setupFlash(w, r, "Установка: ошибка записи файла: "+err.Error())
|
||||
return
|
||||
}
|
||||
out.Close()
|
||||
|
||||
// Распаковка (если .tar/.tgz/.tar.gz).
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Minute)
|
||||
defer cancel()
|
||||
if strings.HasSuffix(strings.ToLower(dst), ".rpm") {
|
||||
// Один rpm — установим напрямую.
|
||||
} else if strings.HasSuffix(strings.ToLower(dst), ".tar.gz") || strings.HasSuffix(strings.ToLower(dst), ".tgz") {
|
||||
if untar, err := runCmdInDir(ctx, dir, "tar", "-xzf", dst); err != nil {
|
||||
setupFlash(w, r, "Установка: распаковка .tgz упала: "+err.Error()+" / вывод: "+untar)
|
||||
return
|
||||
}
|
||||
} else if strings.HasSuffix(strings.ToLower(dst), ".tar") {
|
||||
if untar, err := runCmdInDir(ctx, dir, "tar", "-xf", dst); err != nil {
|
||||
setupFlash(w, r, "Установка: распаковка .tar упала: "+err.Error()+" / вывод: "+untar)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
setupFlash(w, r, "Установка: неизвестный формат файла, нужен .tar/.tgz/.tar.gz/.rpm")
|
||||
return
|
||||
}
|
||||
|
||||
// Найти все .rpm в директории.
|
||||
var rpms []string
|
||||
_ = filepath.Walk(dir, func(p string, info os.FileInfo, err error) error {
|
||||
if err == nil && !info.IsDir() && strings.HasSuffix(p, ".rpm") {
|
||||
rpms = append(rpms, p)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if len(rpms) == 0 {
|
||||
setupFlash(w, r, "Установка: после распаковки .rpm файлы не найдены в "+dir)
|
||||
return
|
||||
}
|
||||
|
||||
// sudo rpm -i <все rpm>. На РЕД ОС иногда нужен --nosignature --nodeps.
|
||||
args := append([]string{"rpm", "-Uvh", "--replacepkgs", "--nosignature"}, rpms...)
|
||||
output, err := runCmd(ctx, "sudo", args...)
|
||||
if err != nil {
|
||||
setupFlash(w, r, "Установка: rpm -i упал: "+err.Error()+" / вывод: "+strings.TrimSpace(output))
|
||||
return
|
||||
}
|
||||
setupFlash(w, r, "КриптоПро CSP установлен. Файлов rpm: "+fmt.Sprint(len(rpms))+". Теперь введите серийник и нажмите «Активировать лицензию». Вывод rpm: "+strings.TrimSpace(output))
|
||||
}
|
||||
|
||||
// runCmdInDir выполняет команду в указанной рабочей директории.
|
||||
func runCmdInDir(ctx context.Context, dir, name string, args ...string) (string, error) {
|
||||
cmd := exec.CommandContext(ctx, name, args...)
|
||||
cmd.Dir = dir
|
||||
out, err := cmd.CombinedOutput()
|
||||
return string(out), err
|
||||
}
|
||||
|
||||
// runCmd выполняет команду и возвращает stdout+stderr строкой.
|
||||
func runCmd(ctx context.Context, name string, args ...string) (string, error) {
|
||||
cmd := exec.CommandContext(ctx, name, args...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
return string(out), err
|
||||
}
|
||||
|
||||
// activateLicense — POST /admin/setup/crypto/activate. Принимает серийный
|
||||
// номер из формы, вызывает cpconfig -license -set, возвращает результат
|
||||
// во flash. Если КриптоПро CSP не установлен — даёт ссылку на инструкцию.
|
||||
func (h *setupHandlers) activateLicense(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
serial := strings.TrimSpace(r.FormValue("license_key"))
|
||||
if serial == "" {
|
||||
setupFlash(w, r, "Активация лицензии: введите серийный номер в поле выше")
|
||||
return
|
||||
}
|
||||
cpconfig := "/opt/cprocsp/sbin/amd64/cpconfig"
|
||||
if _, err := os.Stat(cpconfig); err != nil {
|
||||
setupFlash(w, r, "КриптоПро CSP не установлен ("+cpconfig+" не найден). Раздел /admin/help/cryptopro — команды установки и копирования дистрибутива на ВМ.")
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
out, err := runCmd(ctx, cpconfig, "-license", "-set", serial)
|
||||
if err != nil {
|
||||
setupFlash(w, r, "Активация лицензии не прошла: "+err.Error()+" / вывод: "+strings.TrimSpace(out))
|
||||
return
|
||||
}
|
||||
cur := h.rc.Snapshot().Crypto
|
||||
cur.LicenseKey = serial
|
||||
if err := h.rc.UpdateCrypto(cur); err != nil {
|
||||
log.Printf("activateLicense: UpdateCrypto: %v", err)
|
||||
}
|
||||
setupFlash(w, r, "Лицензия КриптоПро активирована. Вывод cpconfig: "+strings.TrimSpace(out))
|
||||
}
|
||||
|
||||
// checkCrypto — POST /admin/setup/crypto/check. Запускает Health()
|
||||
// текущего провайдера PKCS#11 без изменения настроек.
|
||||
func (h *setupHandlers) checkCrypto(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user