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"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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/postgres", h.savePostgres)
|
||||||
mux.HandleFunc("/admin/setup/crypto", h.saveCrypto)
|
mux.HandleFunc("/admin/setup/crypto", h.saveCrypto)
|
||||||
mux.HandleFunc("/admin/setup/crypto/check", h.checkCrypto)
|
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/nsd", h.saveNSD)
|
||||||
mux.HandleFunc("/admin/setup/lk", h.saveLK)
|
mux.HandleFunc("/admin/setup/lk", h.saveLK)
|
||||||
mux.HandleFunc("/admin/setup/test-run", h.testRun)
|
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()
|
// checkCrypto — POST /admin/setup/crypto/check. Запускает Health()
|
||||||
// текущего провайдера PKCS#11 без изменения настроек.
|
// текущего провайдера PKCS#11 без изменения настроек.
|
||||||
func (h *setupHandlers) checkCrypto(w http.ResponseWriter, r *http.Request) {
|
func (h *setupHandlers) checkCrypto(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -80,6 +80,22 @@
|
|||||||
<button type="submit" class="btn" style="background:var(--border);color:var(--text);border:none;padding:8px 16px;border-radius:4px">Проверить подключение СКЗИ</button>
|
<button type="submit" class="btn" style="background:var(--border);color:var(--text);border:none;padding:8px 16px;border-radius:4px">Проверить подключение СКЗИ</button>
|
||||||
<span class="muted" style="margin-left:8px">Загрузит PKCS#11 модуль, опросит список токенов, покажет результат сверху страницы.</span>
|
<span class="muted" style="margin-left:8px">Загрузит PKCS#11 модуль, опросит список токенов, покажет результат сверху страницы.</span>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<hr style="margin:18px 0;border:none;border-top:1px solid var(--border)">
|
||||||
|
<h3 style="font-size:14px;margin:0 0 8px">Установка КриптоПро CSP</h3>
|
||||||
|
<p class="muted">Дистрибутив с <a href="https://www.cryptopro.ru/products/csp/downloads" target="_blank" rel="noopener">cryptopro.ru</a> (например, <code>linux-amd64.tgz</code> или <code>linux-amd64.tar</code> для РЕД ОС/ALT/ROSA). Загрузите файл здесь — он будет распакован и установлен через <code>sudo rpm -Uvh</code>. Установка длится ~30 секунд.</p>
|
||||||
|
<form method="post" action="/admin/setup/crypto/install" enctype="multipart/form-data" style="margin-top:12px;display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||||||
|
<input type="file" name="dist" accept=".tar,.tgz,.gz,.rpm" required style="padding:6px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px;flex:1;min-width:300px">
|
||||||
|
<button type="submit" class="btn" style="background:var(--accent);color:white;border:none;padding:8px 16px;border-radius:4px">Загрузить и установить</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr style="margin:18px 0;border:none;border-top:1px solid var(--border)">
|
||||||
|
<h3 style="font-size:14px;margin:0 0 8px">Активация лицензии</h3>
|
||||||
|
<form method="post" action="/admin/setup/crypto/activate" style="margin-top:6px;display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||||||
|
<input type="text" name="license_key" value="{{.Settings.Crypto.LicenseKey}}" placeholder="XXXX-XXXXX-XXXXX-XXXXX-XXXXX (серийный номер КриптоПро CSP)" style="padding:8px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px;font-family:monospace;font-size:12px;min-width:340px">
|
||||||
|
<button type="submit" class="btn" style="background:var(--ok);color:#0a0f1a;border:none;padding:8px 16px;border-radius:4px;font-weight:600">Активировать лицензию</button>
|
||||||
|
</form>
|
||||||
|
<p class="muted" style="margin-top:8px">Вызовет <code>cpconfig -license -set</code> и сохранит серийник. Если КриптоПро CSP ещё не установлен — покажет инструкцию.</p>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user