Files
Bridge-and-Join-s/internal/lkgateway/lkdist.go
T

335 lines
17 KiB
Go

package lkgateway
// lkdist.go — дистрибутив кабинета клиента для отдельного веб-сервера.
//
// Кабинет (bj-lk) ставится не рядом с ядром, а на веб-сервер депозитария: своей базы
// у него нет, он обращается к ядру по адресу и ключу. Здесь bj собирает архив с
// исполняемым файлом, юнитом systemd, файлом настроек, установщиком и инструкцией —
// администратору остаётся распаковать архив и запустить установку.
import (
"archive/tar"
"compress/gzip"
"crypto/rand"
"encoding/hex"
"fmt"
"net/http"
"os"
"strings"
"time"
)
// lkDistBinary — где лежит исполняемый файл кабинета на этом сервере (переменная — для тестов).
var lkDistBinary = "/opt/bj/bj-lk"
// lkCoreURL — адрес ядра, по которому кабинет будет за ним ходить. Если в настройках
// не задан — подставляем адрес этого сервера, чтобы инструкция не была пустой.
func lkCoreURL(s Settings, r *http.Request) string {
if u := strings.TrimSpace(s.LK.CoreURL); u != "" {
return strings.TrimRight(u, "/")
}
host := strings.TrimSpace(r.Host)
if host == "" {
host = "bj.example.ru"
}
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
return scheme + "://" + host
}
// lkNewAPIKey — новый ключ доступа модуля. Если источник случайных чисел отказал,
// возвращаем ошибку: предсказуемый ключ хуже, чем его отсутствие.
func lkNewAPIKey() (string, error) {
b := make([]byte, 24)
if _, err := rand.Read(b); err != nil {
return "", fmt.Errorf("нет источника случайных чисел, ключ не создан: %v", err)
}
return "lk-" + hex.EncodeToString(b), nil
}
// lkKeyForPage — ключ для страницы настроек. Показывается целиком: настройки ядра —
// закрытый контур, а при переустановке кабинета ключ нужно переписать в установщик;
// маскированный пришлось бы пересоздавать и менять на всех кабинетах.
func lkKeyForPage(key string) string { return key }
// maskSecret — показать ключ так, чтобы его нельзя было списать со скриншота,
// но можно было сверить с тем, что стоит у кабинета.
func maskSecret(v string) string {
v = strings.TrimSpace(v)
if v == "" {
return ""
}
if len(v) <= 12 {
return strings.Repeat("•", len(v))
}
return v[:7] + strings.Repeat("•", 12) + v[len(v)-4:]
}
// lkDistFiles — содержимое архива: имя файла → содержимое и права.
func lkDistFiles(s Settings, coreURL string) ([]struct {
Name string
Mode int64
Data []byte
}, error) {
bin, err := os.ReadFile(lkDistBinary)
if err != nil {
return nil, fmt.Errorf("исполняемый файл кабинета не найден (%s): %v", lkDistBinary, err)
}
title := strings.TrimSpace(orgName(s))
if title == "" {
title = "Личный кабинет"
} else {
title += " · личный кабинет"
}
mode, urlLine := "direct", "BJ_URL="+coreURL
if s.LK.AgentMode {
// Режим ожидания: адрес ядра кабинету не нужен и не должен быть задан —
// ядро подключается к кабинету само по имени этого сервера.
mode, urlLine = "agent", "# BJ_URL не нужен: ядро подключается к кабинету само"
}
// Ключ в архив НЕ кладём: его вводят в веб-установщике кабинета. Так архив можно
// хранить и пересылать, а секрет живёт только в двух местах — в ядре и в файле
// настроек кабинета с правами 0600.
env := fmt.Sprintf(`# Настройки кабинета клиента (bj-lk) для установки службой. Файл читает служба, права 0600.
# Проще — ничего здесь не менять: после запуска откройте адрес кабинета в браузере,
# запустится установщик и сам спросит способ связи и ключ.
# Режим связи с ядром: direct — кабинет обращается к ядру; agent — ядро подключается сюда.
BJ_MODE=%s
# Адрес ядра bj, к которому обращается кабинет (только для режима direct):
%s
# Ключ доступа модуля — из настроек ядра, раздел «Личный кабинет». Оставьте пустым,
# чтобы ввести его в установщике; заполните — если ставите без браузера.
BJ_KEY=
# Адрес и порт, на которых кабинет слушает (за ним обычно стоит nginx с TLS):
LISTEN=127.0.0.1:8099
# Заголовок кабинета:
LK_TITLE=%s
# Каталог данных (файл настроек установщика):
LK_DATA=/var/lib/bj-lk
`, mode, urlLine, title)
unit := `[Unit]
Description=Личный кабинет клиента депозитария (bj-lk)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=bjlk
Group=bjlk
EnvironmentFile=/etc/bj-lk/lk.env
ExecStart=/opt/bj-lk/bj-lk
Restart=on-failure
RestartSec=5
StandardOutput=append:/var/log/bj-lk/bj-lk.log
StandardError=append:/var/log/bj-lk/bj-lk.log
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/log/bj-lk /var/lib/bj-lk
[Install]
WantedBy=multi-user.target
`
install := `#!/bin/sh
# Установка кабинета клиента на этот сервер. Запускать от root: sudo ./install.sh
set -eu
DIR=$(cd "$(dirname "$0")" && pwd)
id bjlk >/dev/null 2>&1 || useradd --system --no-create-home --shell /usr/sbin/nologin bjlk
install -d -m 755 /opt/bj-lk
install -d -m 750 -o bjlk -g bjlk /var/log/bj-lk
install -d -m 700 -o bjlk -g bjlk /var/lib/bj-lk
install -d -m 750 /etc/bj-lk
install -m 755 "$DIR/bj-lk" /opt/bj-lk/bj-lk
if [ -f /etc/bj-lk/lk.env ]; then
echo "Файл /etc/bj-lk/lk.env уже есть — оставляю как есть."
else
install -m 600 "$DIR/lk.env" /etc/bj-lk/lk.env
fi
install -m 644 "$DIR/bj-lk.service" /etc/systemd/system/bj-lk.service
systemctl daemon-reload
systemctl enable --now bj-lk
sleep 2
systemctl --no-pager --full status bj-lk | head -n 12
echo
echo "Дальше откройте адрес кабинета в браузере — запустится установщик."
echo "Проверка службы: curl -s http://127.0.0.1:8099/health"
`
readme := fmt.Sprintf(`Личный кабинет клиента депозитария (bj-lk)
==========================================
Что это. Кабинет — отдельная программа для веб-сервера депозитария. Своей базы у неё
нет: все данные она запрашивает у ядра bj по защищённому каналу и не хранит дольше
сессии пользователя. Поэтому кабинет можно держать в защищённом сегменте (DMZ), а
учётные данные клиентов оставить внутри.
Что в архиве:
bj-lk — исполняемый файл кабинета (Linux, x86-64)
bj-lk.service — служба systemd
lk.env — настройки: адрес ядра, ключ доступа, порт, заголовок
install.sh — установка одной командой
README.txt — этот файл
Установка — как у обычного веб-приложения:
1. Распакуйте архив на сервере кабинета.
2. sudo ./install.sh — заводит службу; либо просто запустите ./bj-lk в своём
каталоге, если хостинг позволяет держать программу запущенной.
3. Откройте адрес кабинета в браузере. Запустится установщик: проверит сервер,
спросит способ связи с ядром и ключ доступа, проверит связь и предложит
завести администратора кабинета. Настройки он сохранит в файл lk.json
(каталог LK_DATA, права 0600). Ключа в этом архиве нет — берите его из
настроек ядра, раздел «Личный кабинет».
4. Проверка службы: curl -s http://127.0.0.1:8099/health — "ok":true.
Установка без браузера: заполните BJ_KEY (и BJ_URL для режима direct) в lk.env и
перезапустите службу — окружение перекрывает файл установщика.
Настройки (файл /etc/bj-lk/lk.env):
BJ_URL — адрес ядра bj: %s
BJ_KEY — ключ доступа модуля, он же в настройках ядра, раздел «Личный кабинет»
LISTEN — адрес и порт кабинета (по умолчанию 127.0.0.1:8099)
LK_TITLE — заголовок кабинета: %s
После изменения файла: sudo systemctl restart bj-lk
Публикация в интернет. Поставьте перед кабинетом nginx с сертификатом TLS:
server {
listen 443 ssl;
server_name lk.example.ru;
ssl_certificate /etc/ssl/certs/lk.crt;
ssl_certificate_key /etc/ssl/private/lk.key;
location / {
proxy_pass http://127.0.0.1:8099;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Режим связи (BJ_MODE в lk.env). Сейчас в архиве: %s.
direct — кабинет сам обращается к ядру по адресу BJ_URL. Подходит, когда у ядра
есть адрес, доступный серверу кабинета.
agent — у ядра нет адреса в интернете. Тогда кабинет ничего никуда не звонит: он
ждёт, пока ядро само подключится к нему по имени %s, заберёт запросы
клиентов и вернёт ответы. Наружу из сети депозитария идёт только
исходящее соединение, порты открывать не нужно. В настройках ядра
(«Личный кабинет» → связь) выберите «Ядро подключается к кабинету» и
укажите адрес этого сервера.
Сеть и безопасность:
• наружу открыт только 443 сервера кабинета;
• в режиме direct кабинет обращается к ядру на %s — разрешите этот доступ
в межсетевом экране; в режиме agent входящий доступ к ядру не нужен вовсе;
• ключ доступа хранится только в файле настроек кабинета (lk.json или lk.env,
права 0600); в этом архиве его нет;
• ключ можно сменить в настройках ядра, после чего удалить lk.json и пройти
установщик заново (или вписать новый ключ в lk.env) и перезапустить службу;
• кабинет не хранит данные клиентов: ни базы, ни файлов — всё берётся у ядра
на время запроса. Шрифты и оформление вшиты в программу, сторонние сайты
страница не запрашивает;
Обновления. Кабинет обновляется отдельно от ядра: новую версию скачивайте на
странице настроек ядра, раздел «Личный кабинет», и ставьте тем же install.sh —
настройки при этом сохраняются. Версии кабинета и ядра должны быть совместимы;
если ядро старше, кабинет покажет предупреждение администратору.
Лицензия. Кабинет входит в модуль «Личный кабинет клиента» и работает, пока он есть
в лицензии депозитария.
`, coreURL, title, mode, cabinetHint(s), coreURL)
return []struct {
Name string
Mode int64
Data []byte
}{
{"bj-lk/bj-lk", 0o755, bin},
{"bj-lk/bj-lk.service", 0o644, []byte(unit)},
{"bj-lk/lk.env", 0o600, []byte(env)},
{"bj-lk/install.sh", 0o755, []byte(install)},
{"bj-lk/README.txt", 0o644, []byte(readme)},
}, nil
}
// lkDistDownload — GET /admin/setup/lk/dist.tar.gz: архив для установки кабинета.
func (h *setupHandlers) lkDistDownload(w http.ResponseWriter, r *http.Request) {
s := h.rc.Snapshot()
if strings.TrimSpace(s.LK.APIKey) == "" {
setupFlash(w, r, "Сначала создайте ключ доступа кабинета — кнопка «Создать ключ».")
return
}
files, err := lkDistFiles(s, lkCoreURL(s, r))
if err != nil {
setupFlash(w, r, "Дистрибутив кабинета: "+err.Error())
return
}
name := fmt.Sprintf("bj-lk-%s.tar.gz", time.Now().Format("2006-01-02"))
w.Header().Set("Content-Type", "application/gzip")
w.Header().Set("Content-Disposition", `attachment; filename="`+name+`"`)
gz := gzip.NewWriter(w)
defer gz.Close()
tw := tar.NewWriter(gz)
defer tw.Close()
now := time.Now()
for _, f := range files {
if err := tw.WriteHeader(&tar.Header{Name: f.Name, Mode: f.Mode, Size: int64(len(f.Data)), ModTime: now, Typeflag: tar.TypeReg}); err != nil {
return
}
if _, err := tw.Write(f.Data); err != nil {
return
}
}
}
// lkSaveDist — POST /admin/setup/lk/dist: адрес ядра для кабинета и ключ доступа.
func (h *setupHandlers) lkSaveDist(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
s := h.rc.Snapshot().LK
if u := strings.TrimSpace(r.FormValue("core_url")); u != "" || r.FormValue("core_url_set") != "" {
s.CoreURL = strings.TrimRight(u, "/")
}
// Режим связи: кабинет ходит к ядру или ядро подключается к кабинету само.
if r.FormValue("mode_set") != "" {
s.BindNetwork = r.FormValue("bind_network") == "1"
s.AgentMode = r.FormValue("mode") == "agent"
s.CabinetURL = strings.TrimRight(strings.TrimSpace(r.FormValue("cabinet_url")), "/")
if s.AgentMode && s.CabinetURL == "" {
setupFlash(w, r, "Кабинет: для режима ожидания укажите адрес кабинета на хостинге (https://lk.example.ru).")
return
}
}
msg := "Кабинет: настройки сохранены"
switch r.FormValue("op") {
case "newkey":
key, err := lkNewAPIKey()
if err != nil {
setupFlash(w, r, "Кабинет: "+err.Error())
return
}
s.APIKey = key
msg = "Создан новый ключ доступа кабинета. Введите его в установщике кабинета (или в lk.env); если кабинет уже работал — удалите там lk.json и пройдите установщик заново либо впишите ключ в lk.env и перезапустите службу."
case "clearkey":
s.APIKey = ""
msg = "Ключ удалён: кабинет больше не сможет обращаться к ядру."
}
if err := h.rc.UpdateLK(s); err != nil {
setupFlash(w, r, "Кабинет: ошибка сохранения: "+err.Error())
return
}
setupFlash(w, r, msg)
}
// cabinetHint — как в инструкции назвать адрес самого кабинета.
func cabinetHint(s Settings) string {
if u := strings.TrimSpace(s.LK.CabinetURL); u != "" {
return u
}
return "https://lk.example.ru"
}