Первичная настройка, повторная проверка: копии без ключевых носителей СКЗИ (--one-file-system, без keymedia), без отпечатка железа и списка компонентов; восстановление на новый сервер — пароль базы bj из копии, база и пользователь ИШ; ключ для контура без канала запоминается и подключается при настройке канала; первый настроенный канал становится рабочим контуром; справочник сертификатов перечитывается после ключа и смены контура; шлюз НРД — шаг 7, ключ — шаг 8 (ключ ставится в профиль ИШ); подсказка контура ключа по сертификату; ИШ: параметры работающего шлюза не перезаписываются; без чужого кода отправителя и получателя по умолчанию; прокси для выхода в интернет; установка с образа без интернета; HTTPS — новый сертификат при смене адреса; bj-network — без cloud-init; проверка готовности: справочник участников, сетевые доступы, учётные записи, последний обмен с НРД
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
@@ -232,10 +232,15 @@ func (in *installer) stepPrecheck() (string, error) {
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
// прокси организации — из окружения (https_proxy / HTTPS_PROXY), как у curl
|
||||
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, in.server+"/healthz", nil)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("нет связи с %s: %v", in.server, err)
|
||||
if in.bundle != "" {
|
||||
// установка с образа — всё нужное на диске; ключ продукта и обновления — позже, когда будет связь
|
||||
return fmt.Sprintf("%s, %.1f ГБ памяти; нет связи с %s — ставим с образа, ключ продукта введёте в браузере, когда появится доступ (можно через прокси: «Настройка → Сервер»)", name, float64(mem)/(1<<30), in.server), nil
|
||||
}
|
||||
return "", fmt.Errorf("нет связи с %s: %v (если выход в интернет через прокси — запустите с переменной https_proxy=http://прокси:порт)", in.server, err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
return fmt.Sprintf("%s, %.1f ГБ памяти, связь с сервером лицензий есть", name, float64(mem)/(1<<30)), nil
|
||||
|
||||
@@ -37,7 +37,9 @@ const serviceName = "bj-server"
|
||||
|
||||
func main() {
|
||||
addr := getenv("BJ_HTTP_ADDR", ":8080")
|
||||
defaultSender := m2m.DeponentCode(getenv("BJ_M2M_SENDER", "MC0079200000"))
|
||||
// Код отправителя — код участника нашей организации из настроек (шаг «Организация»); переменная — только
|
||||
// для старых установок. Чужого кода по умолчанию нет: без организации переводы не формируются.
|
||||
defaultSender := m2m.DeponentCode(getenv("BJ_M2M_SENDER", ""))
|
||||
defaultReceiver := m2m.DeponentCode(getenv("BJ_M2M_RECEIVER", "MC0010300000"))
|
||||
setupPath := os.Getenv("BJ_SETUP_PATH")
|
||||
|
||||
|
||||
@@ -33,10 +33,16 @@ runuser -u postgres -- pg_dump -Fc bj > "$W/bj.pgdump" || fail "дамп баз
|
||||
if runuser -u postgres -- psql -Atc "select 1 from pg_database where datname='igate'" 2>/dev/null | grep -q 1; then
|
||||
runuser -u postgres -- pg_dump -Fc igate > "$W/igate.pgdump" || fail "дамп базы igate"
|
||||
fi
|
||||
tarok -C / -czf "$W/bj-data.tar.gz" --exclude=etc/bj/backup-smb.cred var/lib/bj/.bj etc/bj || fail "архив данных bj"
|
||||
# Не копируем: пароль хранилища; то, что принадлежит этой машине (отпечаток железа, список установленных
|
||||
# компонентов — на новом сервере они свои); ключевые носители (флешка ключа монтируется в
|
||||
# /var/lib/igate/keymedia): ключи СКЗИ не должны уходить на сетевое хранилище — при восстановлении носитель
|
||||
# подключают заново. --one-file-system — не заходить ни в какие смонтированные носители.
|
||||
tarok -C / -czf "$W/bj-data.tar.gz" --one-file-system --exclude=etc/bj/backup-smb.cred --exclude=etc/bj/hw-id \
|
||||
--exclude=var/lib/bj/.bj/components.json var/lib/bj/.bj etc/bj || fail "архив данных bj"
|
||||
if [ -d /var/lib/igate ]; then
|
||||
tarok -C / -czf "$W/igate-data.tar.gz" --warning=no-file-changed --exclude='var/lib/igate/.cache' \
|
||||
--exclude='var/lib/igate/NSD Integration Gateway/logs*' --exclude='var/lib/igate/logs-cli' var/lib/igate || fail "архив шлюза НРД"
|
||||
tarok -C / -czf "$W/igate-data.tar.gz" --one-file-system --warning=no-file-changed --exclude='var/lib/igate/.cache' \
|
||||
--exclude='var/lib/igate/keymedia/*' --exclude='var/lib/igate/NSD Integration Gateway/logs*' \
|
||||
--exclude='var/lib/igate/logs-cli' var/lib/igate || fail "архив шлюза НРД"
|
||||
fi
|
||||
cat > "$W/backup-info.txt" <<INFO
|
||||
Bridge&Join — резервная копия сервера $(hostname) от $(date '+%d.%m.%Y %H:%M:%S')
|
||||
|
||||
@@ -129,13 +129,43 @@ PY
|
||||
LC_ALL=C PGOPTIONS='-c lc_messages=C' runuser -u postgres -- pg_restore --clean --if-exists -d bj < "$W/bj.pgdump" >/dev/null 2>"$W/pg.err"
|
||||
# «does not exist» при --clean на пустой базе — не ошибка; остальное — ошибка
|
||||
if grep -i "error" "$W/pg.err" | grep -vqi "does not exist"; then die "база bj: $(grep -i error "$W/pg.err" | grep -vi 'does not exist' | head -1)"; fi
|
||||
if [ -s "$W/igate.pgdump" ] && runuser -u postgres -- psql -Atc "select 1 from pg_database where datname='igate'" | grep -q 1; then
|
||||
if [ -s "$W/igate.pgdump" ]; then
|
||||
st db "восстановление базы шлюза НРД"
|
||||
# новый сервер: ИШ ещё не ставили — готовим пользователя, роль, базу и доступ без пароля (как ставит
|
||||
# bj-igate-admin), чтобы данные и каналы ИШ вернулись; сам пакет ИШ потом ставится на шаге «Шлюз НРД»
|
||||
id igate >/dev/null 2>&1 || useradd --system --home-dir /var/lib/igate --create-home --shell /usr/sbin/nologin igate
|
||||
runuser -u postgres -- psql -tAc "select 1 from pg_roles where rolname='igate'" | grep -q 1 || runuser -u postgres -- psql -qc "create role igate login"
|
||||
runuser -u postgres -- psql -lqt | cut -d'|' -f1 | grep -qw igate || runuser -u postgres -- createdb -O igate igate
|
||||
hba=$(runuser -u postgres -- psql -tAc "show hba_file")
|
||||
if [ -f "$hba" ] && ! grep -q '^# bj-igate-trust' "$hba"; then
|
||||
cp -a "$hba" "$hba.bak-bj-igate"
|
||||
{ printf '# bj-igate-trust\nlocal igate igate trust\nhost igate igate 127.0.0.1/32 trust\nhost igate igate ::1/128 trust\n'; cat "$hba.bak-bj-igate"; } > "$hba"
|
||||
runuser -u postgres -- psql -qtAc "select pg_reload_conf()" >/dev/null
|
||||
fi
|
||||
LC_ALL=C PGOPTIONS='-c lc_messages=C' runuser -u postgres -- pg_restore --clean --if-exists -d igate < "$W/igate.pgdump" >/dev/null 2>&1
|
||||
fi
|
||||
st files "восстановление данных bj и шлюза НРД"
|
||||
tar -C / -xzf "$W/bj-data.tar.gz" || die "распаковка данных bj"
|
||||
[ -s "$W/igate-data.tar.gz" ] && { tar -C / -xzf "$W/igate-data.tar.gz" || die "распаковка данных шлюза НРД"; }
|
||||
# своё у этой машины (отпечаток железа, установленные компоненты) — сохраняем поверх копии; ключевые
|
||||
# носители из старых копий не раскладываем: ключ СКЗИ — только на своём носителе
|
||||
mkdir -p "$W/keep"; cp -a /var/lib/bj/.bj/components.json /etc/bj/hw-id "$W/keep/" 2>/dev/null
|
||||
tar -C / -xzf "$W/bj-data.tar.gz" --exclude=etc/bj/hw-id --exclude=var/lib/bj/.bj/components.json || die "распаковка данных bj"
|
||||
[ -f "$W/keep/components.json" ] && cp -a "$W/keep/components.json" /var/lib/bj/.bj/components.json
|
||||
[ -f "$W/keep/hw-id" ] && cp -a "$W/keep/hw-id" /etc/bj/hw-id
|
||||
# пароль пользователя базы bj — как в восстановленных настройках: установщик нового сервера задал свой
|
||||
# случайный пароль, и без этого bj не подключился бы к своей базе (ушёл бы в хранение в памяти)
|
||||
pw=$(python3 - <<'PY'
|
||||
import json,urllib.parse
|
||||
try:
|
||||
d=json.load(open("/var/lib/bj/.bj/setup.json")).get("postgres",{}).get("dsn","")
|
||||
u=urllib.parse.urlparse(d)
|
||||
if u.username=="bj" and u.password and u.hostname in ("127.0.0.1","localhost"): print(urllib.parse.unquote(u.password))
|
||||
except Exception: pass
|
||||
PY
|
||||
)
|
||||
if [ -n "$pw" ]; then
|
||||
printf "ALTER ROLE bj WITH PASSWORD '%s';\n" "$(printf '%s' "$pw" | sed "s/'/''/g")" | runuser -u postgres -- psql -q >/dev/null 2>&1 || die "не удалось выставить пароль базы bj из копии"
|
||||
fi
|
||||
[ -s "$W/igate-data.tar.gz" ] && { tar -C / -xzf "$W/igate-data.tar.gz" --exclude='var/lib/igate/keymedia/*' || die "распаковка данных шлюза НРД"; }
|
||||
# копия хранилища в восстановленных настройках есть, а пароль в неё не входит — берём тот, с которым
|
||||
# восстанавливали, и возвращаем расписание копий
|
||||
if [ -r "$CONF" ]; then
|
||||
@@ -148,7 +178,12 @@ PY
|
||||
systemctl start bj-server >/dev/null 2>&1
|
||||
systemctl is-enabled bj-backup.timer >/dev/null 2>&1 && systemctl start bj-backup.timer >/dev/null 2>&1
|
||||
for u in bj-npr bj-lk; do systemctl is-enabled "$u" >/dev/null 2>&1 && systemctl start "$u" >/dev/null 2>&1; done
|
||||
had_igate=0; [ -s "$W/igate-data.tar.gz" ] && had_igate=1
|
||||
rm -rf "$W" /run/bj-restore.conf
|
||||
st done "восстановлено из копии $NAME" ;;
|
||||
if [ "$had_igate" = 1 ] && [ ! -x /opt/nsd/igate/igate-svc/igate-svc ]; then
|
||||
st done "восстановлено из копии $NAME. Шлюз НРД на этом сервере не установлен — установите его в мастере (шаг «Шлюз НРД»): настройки и каналы из копии подхватятся; ключ — подключите носитель и установите мастером ключа"
|
||||
else
|
||||
st done "восстановлено из копии $NAME"
|
||||
fi ;;
|
||||
*) err "неизвестная команда: $cmd" ;;
|
||||
esac
|
||||
|
||||
@@ -114,9 +114,11 @@ PY
|
||||
st deps "системные пакеты для работы ИШ без графики"
|
||||
need=""
|
||||
for p in gnome-keyring dbus xvfb xauth libsecret-tools unzip curl; do dpkg -s "$p" >/dev/null 2>&1 || need="$need $p"; done
|
||||
px=$(j proxy) # прокси организации (из настроек bj) — для apt и скачивания с nsd.ru
|
||||
aptpx=(); [ -n "$px" ] && aptpx=(-o "Acquire::http::Proxy=$px" -o "Acquire::https::Proxy=$px")
|
||||
if [ -n "$need" ]; then
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends $need || \
|
||||
{ DEBIAN_FRONTEND=noninteractive apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends $need; } || die "не удалось установить пакеты:$need"
|
||||
DEBIAN_FRONTEND=noninteractive apt-get "${aptpx[@]}" install -y -qq --no-install-recommends $need || \
|
||||
{ DEBIAN_FRONTEND=noninteractive apt-get "${aptpx[@]}" update -qq && DEBIAN_FRONTEND=noninteractive apt-get "${aptpx[@]}" install -y -qq --no-install-recommends $need; } || die "не удалось установить пакеты:$need"
|
||||
fi
|
||||
|
||||
st package "дистрибутив ИШ"
|
||||
@@ -125,7 +127,7 @@ PY
|
||||
v=$(printf '%s' "$ver" | tr . -)
|
||||
url="https://www.nsd.ru/media/integratsionnyj-shlyuz/$v/igate-$v-amd64.zip"
|
||||
echo "скачивание $url"
|
||||
curl -fsSL -m 600 -A 'Mozilla/5.0 (X11; Linux x86_64) bj-installer' -H 'Accept: */*' -o "$W/igate.zip" "$url" || die "не удалось скачать дистрибутив ИШ $ver с сайта НРД ($url)"
|
||||
curl -fsSL -m 600 ${px:+-x "$px"} -A 'Mozilla/5.0 (X11; Linux x86_64) bj-installer' -H 'Accept: */*' -o "$W/igate.zip" "$url" || die "не удалось скачать дистрибутив ИШ $ver с сайта НРД ($url)"
|
||||
file="$W/igate.zip"
|
||||
fi
|
||||
case "$file" in
|
||||
@@ -200,6 +202,8 @@ StartLimitBurst=8
|
||||
User=$SVC_USER
|
||||
WorkingDirectory=$HOME_DIR
|
||||
# самоочистка перед стартом: осиротевшие сокеты, keyring, X-локи — иначе перезапуски срываются
|
||||
# каталог сеанса igate (XDG_RUNTIME_DIR): обычно его создаёт systemd-logind (linger), но без него keyring не стартует
|
||||
ExecStartPre=+/usr/bin/install -d -o $SVC_USER -g $SVC_USER -m 700 /run/user/$uid
|
||||
ExecStartPre=-/bin/bash -c 'rm -f /tmp/CoreFxPipe_NSD.IGate.* $HOME_DIR/.local/share/keyrings/login.keyring; find /tmp -maxdepth 1 -name ".X9*-lock" -delete 2>/dev/null; true'
|
||||
ExecStart=/usr/bin/dbus-run-session -- $WRAPPER
|
||||
Restart=on-failure
|
||||
@@ -215,28 +219,31 @@ EOF
|
||||
run_as_igate_keyring "timeout 90 $IGATE_DIR/igate-cli/igate-cli --data $HOME_DIR </dev/null >/dev/null 2>&1; true"
|
||||
psqlq -c "select to_regclass('public.parameters') is not null" | grep -q t || die "ИШ не создал схему базы (igate-cli). Журнал: $LOG"
|
||||
fi
|
||||
# глобальные параметры: Web API на localhost:8090 без авторизации и запуск движка без графики
|
||||
# Общие параметры ИШ. Обязательные для работы без графики и для bj (движок при старте, Web API на
|
||||
# localhost:8090 без авторизации) — выставляем всегда; остальные — только если их ещё нет: настройки ИШ,
|
||||
# который у клиента уже работает (очистка, хранение, прокси), не трогаем. Значения по умолчанию — эталон GUI.
|
||||
psqlq <<'SQL' || die "не записаны параметры ИШ"
|
||||
DELETE FROM parameters WHERE chanel_id IS NULL AND name IN ('runEngineOnStartApp','server.useServer','server.host','server.port','server.scheme',
|
||||
'server.authentication.enable','server.authentication.userName','server.authentication.password','server.certificate.storage',
|
||||
'server.certificate.store.location','server.certificate.store.name','server.certificate.file.path','server.certificate.file.password',
|
||||
'wsl.httpsMode','wsl.maxConnsPerServer','wsl.proxy.mode','wsl.proxy.address','wsl.proxy.port','wsl.proxy.username','wsl.proxy.password',
|
||||
'enableDbLogging','cleanAutomatically','cleanAtTime','cleanWhenLarger','cleanVacuum','storePeriod','archiveAutomatically');
|
||||
INSERT INTO parameters(name, value, chanel_id) VALUES
|
||||
('runEngineOnStartApp','True',NULL),('server.useServer','True',NULL),('server.host','localhost',NULL),('server.port','8090',NULL),
|
||||
('server.scheme','Http',NULL),('server.authentication.enable','False',NULL),('server.authentication.userName','',NULL),
|
||||
('server.authentication.password','',NULL),('server.certificate.storage','File',NULL),('server.certificate.store.location','CurrentUser',NULL),
|
||||
('server.certificate.store.name','My',NULL),('server.certificate.file.path','',NULL),('server.certificate.file.password','',NULL),
|
||||
('wsl.httpsMode','Auto',NULL),('wsl.maxConnsPerServer','4',NULL),('wsl.proxy.mode','None',NULL),('wsl.proxy.address','',NULL),
|
||||
('wsl.proxy.port','0',NULL),('wsl.proxy.username','',NULL),('wsl.proxy.password','',NULL),('enableDbLogging','False',NULL),
|
||||
('cleanAutomatically','True',NULL),('cleanAtTime','00:30:00',NULL),('cleanWhenLarger','1024',NULL),('cleanVacuum','False',NULL),
|
||||
('storePeriod','30',NULL),('archiveAutomatically','False',NULL);
|
||||
CREATE TEMP TABLE must(n text, v text);
|
||||
INSERT INTO must VALUES ('runEngineOnStartApp','True'),('server.useServer','True'),('server.host','localhost'),
|
||||
('server.port','8090'),('server.scheme','Http'),('server.authentication.enable','False');
|
||||
CREATE TEMP TABLE dflt(n text, v text);
|
||||
INSERT INTO dflt VALUES ('server.authentication.userName',''),('server.authentication.password',''),('server.certificate.storage','File'),
|
||||
('server.certificate.store.location','CurrentUser'),('server.certificate.store.name','My'),('server.certificate.file.path',''),
|
||||
('server.certificate.file.password',''),('wsl.httpsMode','Auto'),('wsl.maxConnsPerServer','4'),('wsl.proxy.mode','None'),
|
||||
('wsl.proxy.address',''),('wsl.proxy.port','0'),('wsl.proxy.username',''),('wsl.proxy.password',''),('enableDbLogging','False'),
|
||||
('cleanAutomatically','False'),('cleanAtTime','00:30:00'),('cleanWhenLarger','1024'),('cleanVacuum','False'),('storePeriod','15'),
|
||||
('archiveAutomatically','False');
|
||||
UPDATE parameters p SET value = m.v FROM must m WHERE p.chanel_id IS NULL AND p.name = m.n;
|
||||
INSERT INTO parameters(name, value, chanel_id)
|
||||
SELECT x.n, x.v, NULL FROM (SELECT * FROM must UNION ALL SELECT * FROM dflt) x
|
||||
WHERE NOT EXISTS (SELECT 1 FROM parameters p WHERE p.chanel_id IS NULL AND p.name = x.n);
|
||||
SQL
|
||||
|
||||
st start "запуск ИШ"
|
||||
systemctl enable igate >/dev/null 2>&1
|
||||
systemctl restart igate || die "служба igate не запустилась"
|
||||
wait_engine 120 || die "ИШ запущен, но движок не перешёл в Running за 2 минуты. Журнал ИШ: $DATA_DIR/logs-svc/log.txt"
|
||||
case "$(j file)" in /var/lib/bj/igate-upload/*) rm -f "$(j file)" ;; esac # загруженный архив больше не нужен
|
||||
st done "ИШ $(igate_version) установлен и работает; дальше — канал контура (TEST3 или боевой)" ;;
|
||||
|
||||
contour)
|
||||
@@ -247,6 +254,8 @@ SQL
|
||||
[[ "$dep" =~ ^[A-Z]{2}[0-9]{10}$ ]] || err "депозитарный код — вида MC0000000000"
|
||||
[[ -z "$prof" || "$prof" =~ ^[A-Za-z0-9_-]+$ ]] || err "недопустимое имя профиля Валидаты"
|
||||
[ -x "$IGATE_DIR/igate-svc/igate-svc" ] || err "ИШ не установлен"
|
||||
# ключ этого контура ставили раньше, чем канал: профиль запомнен установкой ключа
|
||||
if [ -z "$prof" ] && [ -s "$HOME_DIR/.bj-profile-$CODE" ]; then prof=$(tr -cd 'A-Za-z0-9_-' < "$HOME_DIR/.bj-profile-$CODE"); fi
|
||||
DIR=$(exchange_dir "$contour"); install -d -o "$SVC_USER" -g "$SVC_USER" -m 755 "$DIR"
|
||||
psqlq -v code="$CODE$dep" -v name="$NAME" -v ep="$EP" -v fp="$FP" -v typ="$TYPE" -v dep="$dep" -v prof="$prof" -v dir="$DIR" <<'SQL' || err "канал не записан в базу ИШ"
|
||||
BEGIN;
|
||||
|
||||
@@ -199,7 +199,15 @@ open(ini,"w").write("\n".join(L))
|
||||
PY
|
||||
fi
|
||||
# подключить профиль к ИШ: cryptography.profile в БД igate (канал 33)
|
||||
if [ "$IGATE_RES" = "ok" ]; then
|
||||
nchan=1
|
||||
if [ "$IGATE_RES" = "ok" ] && [ -n "$CHAN_PREFIX" ]; then
|
||||
nchan=$(su -s /bin/sh postgres -c "psql -d igate -tAc \"select count(*) from channels where code like '${CHAN_PREFIX}%'\"" 2>/dev/null | tr -d '[:space:]')
|
||||
fi
|
||||
if [ "$IGATE_RES" = "ok" ] && [ "${nchan:-0}" = 0 ]; then
|
||||
# канала этого контура в ИШ ещё нет: профиль запоминаем — bj-igate-admin подключит его при настройке канала
|
||||
printf '%s\n' "$PROFILE" > "/var/lib/igate/.bj-profile-$CHAN_PREFIX"; chown igate:igate "/var/lib/igate/.bj-profile-$CHAN_PREFIX" 2>/dev/null
|
||||
IGATE_RES="pending-channel"
|
||||
elif [ "$IGATE_RES" = "ok" ]; then
|
||||
# только канал контура этого ключа (BJ_CHANNEL); старый bj без контура — все каналы, как раньше. Канал с ключом включаем.
|
||||
if su -s /bin/sh postgres -c "psql -d igate -tAc \"update parameters set value='$PROFILE' where name='cryptography.profile' and $(chan_where); update channels set enable=true where id in (select chanel_id from parameters where name='cryptography.profile' and value='$PROFILE'); update parameters set value='True' where name='enable' and chanel_id in (select chanel_id from parameters where name='cryptography.profile' and value='$PROFILE')\"" >/dev/null 2>&1; then
|
||||
IGATE_RES="connected"
|
||||
|
||||
@@ -26,11 +26,14 @@ F=/etc/netplan/60-bj-static.yaml
|
||||
chmod 600 "$F"
|
||||
# прежние файлы с DHCP для этого интерфейса — выключаем, чтобы не было двух адресов
|
||||
for f in /etc/netplan/*.yaml; do [ "$f" = "$F" ] && continue; grep -q "$IF" "$f" 2>/dev/null && mv "$f" "$f.disabled-by-bj"; done
|
||||
# на Ubuntu с образа сетью управляет cloud-init: без запрета при перезагрузке он может вернуть DHCP
|
||||
CI=/etc/cloud/cloud.cfg.d/99-bj-disable-network-config.cfg
|
||||
[ -d /etc/cloud/cloud.cfg.d ] && printf 'network: {config: disabled}\n' > "$CI"
|
||||
echo " Применяю. Если связь пропадёт — через 120 секунд вернутся прежние настройки."
|
||||
if netplan try --timeout 120; then
|
||||
echo " ✓ Адрес ${ADDR%/*} закреплён. Веб-вход: https://${ADDR%/*}:8443/"
|
||||
[ -x /usr/local/sbin/bj-welcome ] && /usr/local/sbin/bj-welcome
|
||||
else
|
||||
for f in /etc/netplan/*.disabled-by-bj; do [ -f "$f" ] && mv "$f" "${f%.disabled-by-bj}"; done
|
||||
rm -f "$F"; netplan apply; echo " Настройки не приняты — оставлен прежний адрес."
|
||||
rm -f "$F" "$CI"; netplan apply; echo " Настройки не приняты — оставлен прежний адрес."
|
||||
fi
|
||||
|
||||
@@ -16,8 +16,6 @@ RestartSec=5
|
||||
# Конфигурация — через ENV или ~/.bj/setup.json (UI /admin/setup).
|
||||
Environment=BJ_HTTP_ADDR=:8080
|
||||
Environment=BJ_SETUP_PATH=/var/lib/bj/.bj/setup.json
|
||||
Environment=BJ_M2M_SENDER=MC0079200000
|
||||
Environment=BJ_M2M_RECEIVER=MC0010300000
|
||||
|
||||
# КриптоПро CSP кладёт .so в /opt/cprocsp/lib/amd64 без записи в
|
||||
# /etc/ld.so.conf.d. Чтобы Go-PKCS#11 клиент (cryptocli) нашёл
|
||||
|
||||
@@ -93,6 +93,11 @@ func registerFirstRunRestore(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/first-run/backup/list", op("list"))
|
||||
mux.HandleFunc("/first-run/backup/restore", op("restore"))
|
||||
mux.HandleFunc("/first-run/backup/status", func(w http.ResponseWriter, r *http.Request) {
|
||||
// без входа — только пока сервер ждёт первичной настройки или восстановление шло в последние полчаса
|
||||
if st, err := os.Stat(restoreStatusFile); !firstRunPending() && (err != nil || time.Since(st.ModTime()) > 30*time.Minute) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
out := map[string]any{}
|
||||
if raw, err := os.ReadFile(restoreStatusFile); err == nil {
|
||||
var rs map[string]any
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package lkgateway
|
||||
|
||||
// Интеграционный шлюз НРД (ИШ, служба igate) на этом сервере: состояние для мастера (шаг 8).
|
||||
// Интеграционный шлюз НРД (ИШ, служба igate) на этом сервере: состояние для мастера (шаг 7).
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -112,7 +112,10 @@ func registerIgateAdmin(mux *http.ServeMux, rc *RuntimeConfig) {
|
||||
return
|
||||
}
|
||||
s := rc.Snapshot()
|
||||
res["active_contour"] = contourOf(s.NSD.Profile)
|
||||
res["active_contour"] = ""
|
||||
if s.NSD.Profile != "" {
|
||||
res["active_contour"] = contourOf(s.NSD.Profile)
|
||||
}
|
||||
res["active_profile"] = s.NSD.Profile
|
||||
res["deponent"] = pickStr(s.NSD.DeponentCode, orgCode(s))
|
||||
res["depo_db"] = s.Fancy.DepoDB()
|
||||
@@ -153,6 +156,9 @@ func registerIgateAdmin(mux *http.ServeMux, rc *RuntimeConfig) {
|
||||
jsonOut(w, map[string]any{"ok": false, "error": "загрузите архив ИШ или укажите версию вида 107.0.930"})
|
||||
return
|
||||
}
|
||||
if u := rc.Snapshot().Proxy.url(); u != nil {
|
||||
params["proxy"] = u.String() // скачивание с nsd.ru и пакеты — через прокси организации
|
||||
}
|
||||
extra, _ := json.Marshal(params)
|
||||
ctx, cancel := context.WithTimeout(r.Context(), time.Minute)
|
||||
defer cancel()
|
||||
@@ -190,7 +196,21 @@ func registerIgateAdmin(mux *http.ServeMux, rc *RuntimeConfig) {
|
||||
extra, _ := json.Marshal(map[string]string{"contour": contour, "crypto": crypto, "deponent": dep})
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Minute)
|
||||
defer cancel()
|
||||
jsonOut(w, opResult(runKeymediaOp(ctx, "igate-contour", map[string]string{"extra": string(extra)})))
|
||||
res, err := runKeymediaOp(ctx, "igate-contour", map[string]string{"extra": string(extra)})
|
||||
if err == nil && res["ok"] == true && rc.Snapshot().NSD.Profile == "" {
|
||||
// контур в bj ещё не выбран (новая установка) — первый настроенный канал и есть рабочий
|
||||
n := rc.Snapshot().NSD
|
||||
n.Profile, n.IGWBaseURL = contour+"-"+crypto, pickStr(n.IGWBaseURL, igateDefaultURL)
|
||||
if rc.UpdateNSD(n) == nil {
|
||||
res["output"] = asString(res["output"]) + ". bj работает с этим контуром и перезапускается — обновите страницу через 15 секунд"
|
||||
go func() {
|
||||
time.Sleep(2 * time.Second)
|
||||
_, _ = restartUnit(context.Background(), "bj-server")
|
||||
}()
|
||||
}
|
||||
}
|
||||
refreshCertStore(rc)
|
||||
jsonOut(w, opResult(res, err))
|
||||
})
|
||||
// переключить bj на контур: профиль отправки, база DEPO контура; bj перезапускается
|
||||
mux.HandleFunc("/admin/setup/igate/switch", func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -256,6 +276,9 @@ func registerIgateAdmin(mux *http.ServeMux, rc *RuntimeConfig) {
|
||||
n.Profile = contour + "-" + crypto
|
||||
n.IGWBaseURL = pickStr(n.IGWBaseURL, igateDefaultURL)
|
||||
n.SwitchedAt = time.Now()
|
||||
nc := s.NSDCerts
|
||||
nc.LastStore, nc.LastRows, nc.LastProfile = nil, nil, "" // справочник другого контура — перечитается после перезапуска
|
||||
_ = rc.UpdateNSDCerts(nc)
|
||||
if target != "" && target != f.DepoDB() {
|
||||
f.DepoDatabase = target
|
||||
f.DepoTablesCheck, f.DepoTablesCheckOK = "", false // таблицы и обработку в другой базе — проверить заново
|
||||
@@ -278,6 +301,23 @@ func registerIgateAdmin(mux *http.ServeMux, rc *RuntimeConfig) {
|
||||
}
|
||||
}()
|
||||
name := map[string]string{"prod": "боевой", "test3": "тестовый TEST3"}[contour]
|
||||
jsonOut(w, map[string]any{"ok": true, "output": "bj переключён на " + name + " контур (база DEPO " + rc.Snapshot().Fancy.DepoDB() + ") и перезапускается — обновите страницу через 15 секунд"})
|
||||
msg := "bj переключён на " + name + " контур (база DEPO " + rc.Snapshot().Fancy.DepoDB() + ") и перезапускается — обновите страницу через 15 секунд"
|
||||
if !rc.Snapshot().Fancy.DepoTablesCheckOK {
|
||||
msg += ". База DEPO сменилась — пройдите шаг 3 (таблицы обмена и обработка в этой базе)"
|
||||
}
|
||||
jsonOut(w, map[string]any{"ok": true, "output": msg})
|
||||
})
|
||||
}
|
||||
|
||||
// refreshCertStore — перечитать справочник сертификатов ИШ сейчас (после установки ключа, настройки канала,
|
||||
// смены контура), не дожидаясь плановой сверки раз в 6 часов: от него зависят шаг «Сертификаты» и сроки.
|
||||
func refreshCertStore(rc *RuntimeConfig) {
|
||||
go func() {
|
||||
time.Sleep(20 * time.Second) // ИШ перезапускается после изменений
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
|
||||
defer cancel()
|
||||
if _, err := nsdCertsCheck(ctx, rc); err != nil {
|
||||
log.Printf("nsdcerts: перечитать справочник: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -101,8 +101,41 @@ type flashDrive struct {
|
||||
// keyWizardData — данные шаблона admin_keywizard.html.
|
||||
type keyWizardData struct {
|
||||
page
|
||||
State *keyWizardState
|
||||
Drives []flashDrive
|
||||
State *keyWizardState
|
||||
Drives []flashDrive
|
||||
SuggestContour string // контур по сертификату в архиве: test3 (издатель «Тестовый УЦ») | prod | "" (не понятно)
|
||||
SuggestIssuer string
|
||||
}
|
||||
|
||||
// keyArchiveContour — контур ключа по сертификатам в распакованном архиве (не самоподписанным — это УЦ):
|
||||
// издатель «Тестовый УЦ» — TEST3, иначе боевой. ГОСТ-сертификаты разбираются по TBS (parseNSDCert).
|
||||
func keyArchiveContour(dir string) (contour, issuer string) {
|
||||
_ = filepath.Walk(dir, func(p string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() || info.Size() > 64<<10 || contour != "" {
|
||||
return nil
|
||||
}
|
||||
switch strings.ToLower(filepath.Ext(p)) {
|
||||
case ".cer", ".crt", ".pem":
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
raw, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
ci, _, err := parseNSDCert(raw)
|
||||
if err != nil || ci.SelfSigned || ci.IssuerCN == "" {
|
||||
return nil
|
||||
}
|
||||
issuer = ci.IssuerCN
|
||||
if strings.Contains(strings.ToLower(ci.IssuerCN), "тестов") {
|
||||
contour = "test3"
|
||||
} else {
|
||||
contour = "prod"
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return contour, issuer
|
||||
}
|
||||
|
||||
const keymediaMount = "/var/lib/igate/keymedia"
|
||||
@@ -203,7 +236,11 @@ func (h *setupHandlers) renderKeyWizard(w http.ResponseWriter, r *http.Request)
|
||||
if st.StagingID != "" && !st.Done {
|
||||
drives = listFlashDrives()
|
||||
}
|
||||
render(w, h.tpl.a.keyWizard, keyWizardData{page: nowPage("Установка ключа", "setup"), State: st, Drives: drives})
|
||||
d := keyWizardData{page: nowPage("Установка ключа", "setup"), State: st, Drives: drives}
|
||||
if st.StagingID != "" && !st.Done {
|
||||
d.SuggestContour, d.SuggestIssuer = keyArchiveContour(filepath.Join("/var/lib/bj/media/iso", st.StagingID))
|
||||
}
|
||||
render(w, h.tpl.a.keyWizard, d)
|
||||
}
|
||||
|
||||
func (h *setupHandlers) keyWizardReset(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -384,6 +421,7 @@ func (h *setupHandlers) keyWizardInstall(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// Ключ уже на флешке и в ИШ — распаковка staging больше не нужна.
|
||||
_ = os.RemoveAll(staging)
|
||||
refreshCertStore(h.rc) // шаг «Сертификаты» и сроки — сразу по новому ключу, а не через 6 часов
|
||||
|
||||
final := "Готово! Ключ на флешке, справочник сформирован, ИШ перезапущен и работает."
|
||||
if dev {
|
||||
@@ -472,6 +510,8 @@ func igateRu(s string) string {
|
||||
switch s {
|
||||
case "connected":
|
||||
return "сформирован, зарегистрирован и подключён к ИШ (cryptography.profile):"
|
||||
case "pending-channel":
|
||||
return "сформирован и зарегистрирован; канала этого контура в ИШ ещё нет — ключ подключится, когда вы настроите канал на шаге «Шлюз НРД»:"
|
||||
case "ok":
|
||||
return "сформирован и зарегистрирован (но не выбран в ИШ):"
|
||||
case "skip":
|
||||
|
||||
@@ -3,6 +3,7 @@ package lkgateway
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
@@ -413,6 +414,10 @@ func pullOneLKClaim(ctx context.Context, rc *RuntimeConfig, svc *Service, ep LKE
|
||||
}
|
||||
req, err := svc.BuildRequest(ctx, claimReq)
|
||||
if err != nil {
|
||||
if errors.Is(err, errReceiverUnknown) { // справочник есть, а депозитария в нём нет — повтор не поможет
|
||||
lkRejectInvalid(ctx, rc, ep, id, &lkDataError{Code: "M2M15", Text: "депозитарий получателя не найден в справочнике участников M2M НРД — проверьте ИНН депозитария получателя"})
|
||||
return "rejected", nil
|
||||
}
|
||||
if strings.Contains(err.Error(), "невалиден") { // данные не проходят формат НРД — повтор не поможет
|
||||
lkRejectInvalid(ctx, rc, ep, id, &lkDataError{Text: "данные заявки не прошли проверку формата НРД: " + err.Error()})
|
||||
return "rejected", nil
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package lkgateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -144,3 +146,52 @@ func TestHTTPSSelfSignedAndRedirect(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReceiverUnknownRejected(t *testing.T) {
|
||||
if !handbookLoaded() {
|
||||
t.Skip("справочник участников не вшит")
|
||||
}
|
||||
svc := &Service{defaultSender: "MC0413600000", defaultReceiver: "MC0010300000"}
|
||||
if _, err := svc.BuildRequest(context.Background(), CreateClaimRequest{ReceivingDepositoryINN: "0000000001"}); err == nil || !errors.Is(err, errReceiverUnknown) {
|
||||
t.Fatalf("неизвестный депозитарий получателя должен давать errReceiverUnknown, а не чужой код: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRequestNeedsOrganization(t *testing.T) {
|
||||
svc := &Service{}
|
||||
if _, err := svc.BuildRequest(context.Background(), CreateClaimRequest{}); err == nil || !strings.Contains(err.Error(), "организация не задана") {
|
||||
t.Fatalf("без кода отправителя перевод не должен формироваться: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyBypass(t *testing.T) {
|
||||
for host, want := range map[string]bool{"localhost": true, "10.10.10.21": true, "192.168.1.5": true, "127.0.0.1": true, "sql002": true, "bj.local": true,
|
||||
"lic.zetit.ru": false, "www.nsd.ru": false, "8.8.8.8": false, "corp.example.ru": true, "a.corp.example.ru": true} {
|
||||
if got := proxyBypass(host, "corp.example.ru"); got != want {
|
||||
t.Errorf("proxyBypass(%s)=%v, want %v", host, got, want)
|
||||
}
|
||||
}
|
||||
p := ProxySettings{Address: "proxy.example.ru:3128", User: "u", Password: "p@ss"}
|
||||
if u := p.url(); u == nil || u.String() != "http://u:p%40ss@proxy.example.ru:3128" {
|
||||
t.Errorf("url: %v", u)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyArchiveContour(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
raw, err := nsdCertsFS.ReadFile("nsd-certs/nsd-test-gost-petrov-20240924.cer")
|
||||
if err != nil {
|
||||
t.Skip(err)
|
||||
}
|
||||
_ = os.MkdirAll(filepath.Join(dir, "spr1"), 0o755)
|
||||
_ = os.WriteFile(filepath.Join(dir, "spr1", "user.cer"), raw, 0o644)
|
||||
if c, iss := keyArchiveContour(dir); c != "test3" || iss == "" {
|
||||
t.Fatalf("тестовый сертификат: %q %q", c, iss)
|
||||
}
|
||||
raw, _ = nsdCertsFS.ReadFile("nsd-certs/nsd-prod-gost-shlyappo-20260821.cer")
|
||||
dir2 := t.TempDir()
|
||||
_ = os.WriteFile(filepath.Join(dir2, "user.cer"), raw, 0o644)
|
||||
if c, _ := keyArchiveContour(dir2); c != "prod" {
|
||||
t.Fatalf("боевой сертификат: %q", c)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package lkgateway
|
||||
|
||||
// Прокси для выхода в интернет (05.10.2026). Во многих организациях сервер ходит в интернет только через
|
||||
// прокси: без него не активировать ключ продукта, не получить обновления, не скачать ИШ и документы НРД.
|
||||
// Прокси из настроек действует на всё, что bj открывает в интернете; адреса локальной сети (шлюз НРД, учёт,
|
||||
// модули, свой кабинет) — напрямую. Не задан — как раньше (переменные окружения службы).
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ProxySettings — прокси организации.
|
||||
type ProxySettings struct {
|
||||
Address string `json:"address,omitempty"` // http://proxy.corp.local:3128
|
||||
User string `json:"user,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
NoProxy string `json:"no_proxy,omitempty"` // ещё адреса напрямую, через запятую: corp.local, 10.20.0.5
|
||||
}
|
||||
|
||||
// url — адрес прокси с логином (nil — прокси не задан).
|
||||
func (p ProxySettings) url() *url.URL {
|
||||
a := strings.TrimSpace(p.Address)
|
||||
if a == "" {
|
||||
return nil
|
||||
}
|
||||
if !strings.Contains(a, "://") {
|
||||
a = "http://" + a
|
||||
}
|
||||
u, err := url.Parse(a)
|
||||
if err != nil || u.Host == "" {
|
||||
return nil
|
||||
}
|
||||
if p.User != "" {
|
||||
u.User = url.UserPassword(p.User, p.Password)
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
// proxyBypass — адрес локальной сети или из списка «напрямую».
|
||||
func proxyBypass(host, extra string) bool {
|
||||
host = strings.ToLower(strings.TrimSpace(host))
|
||||
if host == "" || host == "localhost" || !strings.Contains(host, ".") || strings.HasSuffix(host, ".local") {
|
||||
return true
|
||||
}
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast()
|
||||
}
|
||||
for _, x := range strings.Split(extra, ",") {
|
||||
x = strings.ToLower(strings.TrimPrefix(strings.TrimSpace(x), "."))
|
||||
if x != "" && (host == x || strings.HasSuffix(host, "."+x)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// proxyFunc — прокси из настроек bj (без них — напрямую).
|
||||
func proxyFunc(rc *RuntimeConfig) func(*http.Request) (*url.URL, error) {
|
||||
return func(req *http.Request) (*url.URL, error) {
|
||||
p := rc.Snapshot().Proxy
|
||||
u := p.url()
|
||||
if u == nil || proxyBypass(req.URL.Hostname(), p.NoProxy) {
|
||||
return nil, nil
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
}
|
||||
|
||||
// applyProxy — прокси настроек для всех HTTP-запросов bj (сервер лицензий, обновления, сайт НРД).
|
||||
func applyProxy(rc *RuntimeConfig) {
|
||||
if t, ok := http.DefaultTransport.(*http.Transport); ok {
|
||||
env, mine := t.Proxy, proxyFunc(rc)
|
||||
t.Proxy = func(req *http.Request) (*url.URL, error) {
|
||||
if rc.Snapshot().Proxy.url() != nil {
|
||||
return mine(req)
|
||||
}
|
||||
if env != nil {
|
||||
return env(req)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
if t, ok := noProxyClient.Transport.(*http.Transport); ok {
|
||||
t.Proxy = proxyFunc(rc) // документы НРД: прокси окружения не берём (он может блокировать nsd.ru), прокси bj — да
|
||||
}
|
||||
}
|
||||
|
||||
func registerProxy(mux *http.ServeMux, rc *RuntimeConfig) {
|
||||
mux.HandleFunc("/admin/setup/proxy", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
p := rc.Snapshot().Proxy
|
||||
p.Address = strings.TrimSpace(r.FormValue("address"))
|
||||
p.User = strings.TrimSpace(r.FormValue("user"))
|
||||
if pw := r.FormValue("password"); pw != "" || p.User == "" {
|
||||
p.Password = pw // пароль в страницу не выводится: пусто — прежний
|
||||
}
|
||||
p.NoProxy = strings.TrimSpace(r.FormValue("no_proxy"))
|
||||
if p.Address != "" && p.url() == nil {
|
||||
setupFlash(w, r, "Прокси: адрес вида http://proxy.example.ru:3128")
|
||||
return
|
||||
}
|
||||
if err := rc.UpdateProxy(p); err != nil {
|
||||
setupFlash(w, r, "Прокси: "+err.Error())
|
||||
return
|
||||
}
|
||||
if p.Address == "" {
|
||||
setupFlash(w, r, "Прокси выключен: bj выходит в интернет напрямую")
|
||||
return
|
||||
}
|
||||
// проверка: сервер лицензий через прокси
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
|
||||
defer cancel()
|
||||
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "https://lic.zetit.ru/healthz", nil)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
setupFlash(w, r, "Прокси сохранён, но сервер лицензий через него недоступен: "+err.Error())
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
setupFlash(w, r, fmt.Sprintf("Прокси сохранён: сервер лицензий через него отвечает (HTTP %d)", resp.StatusCode))
|
||||
})
|
||||
}
|
||||
@@ -7,7 +7,9 @@ package lkgateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"sort"
|
||||
@@ -119,7 +121,7 @@ func readinessChecks(ctx context.Context, rc *RuntimeConfig) []ReadyCheck {
|
||||
return c
|
||||
})
|
||||
run(func() ReadyCheck {
|
||||
c := ReadyCheck{Order: 7, Title: "Сертификат ключа организации", Link: "/admin/wizard?step=7", Required: true}
|
||||
c := ReadyCheck{Order: 8, Title: "Сертификат ключа организации", Link: "/admin/wizard?step=8", Required: true}
|
||||
own := ownValidCerts(storeCertsForUI(s))
|
||||
if len(own) == 0 {
|
||||
c.State, c.Detail = "err", "в справочнике ИШ нет действующего сертификата организации — установите ключ мастером установки ключа"
|
||||
@@ -138,7 +140,7 @@ func readinessChecks(ctx context.Context, rc *RuntimeConfig) []ReadyCheck {
|
||||
return c
|
||||
})
|
||||
run(func() ReadyCheck {
|
||||
c := ReadyCheck{Order: 8, Title: "Шлюз НРД", Link: "/admin/wizard?step=8", Required: true}
|
||||
c := ReadyCheck{Order: 7, Title: "Шлюз НРД", Link: "/admin/wizard?step=7", Required: true}
|
||||
up, msg := igateState(ctx, s.NSD.IGWBaseURL)
|
||||
switch {
|
||||
case !up:
|
||||
@@ -147,6 +149,12 @@ func readinessChecks(ctx context.Context, rc *RuntimeConfig) []ReadyCheck {
|
||||
c.State, c.Detail = "err", "шлюз отвечает, но контур НРД в bj не выбран"
|
||||
default:
|
||||
c.State, c.Detail = "ok", msg+"; контур "+s.NSD.Profile
|
||||
nsdWatch.Lock()
|
||||
okAt := nsdWatch.LastPollOK
|
||||
nsdWatch.Unlock()
|
||||
if !okAt.IsZero() {
|
||||
c.Detail += "; последний успешный обмен с НРД " + okAt.Format("02.01 15:04")
|
||||
}
|
||||
if down, det := nsdLinkStatus(); down {
|
||||
c.State, c.Detail = "err", det
|
||||
}
|
||||
@@ -221,11 +229,105 @@ func readinessChecks(ctx context.Context, rc *RuntimeConfig) []ReadyCheck {
|
||||
}
|
||||
return c
|
||||
})
|
||||
run(func() ReadyCheck {
|
||||
c := ReadyCheck{Order: 13, Title: "Справочник участников M2M НРД", Link: "/admin/handbook"}
|
||||
raw, _ := readHandbook()
|
||||
at := handbookAt(raw)
|
||||
var hb struct {
|
||||
Participants []HandbookEntry `json:"participants"`
|
||||
}
|
||||
_ = json.Unmarshal(raw, &hb)
|
||||
switch {
|
||||
case len(hb.Participants) == 0:
|
||||
c.State, c.Detail = "err", "справочника нет — получателя перевода не определить"
|
||||
case !at.IsZero() && time.Since(at) > 14*24*time.Hour:
|
||||
c.State, c.Detail = "warn", fmt.Sprintf("%d участников, от %s — давно не обновлялся (обновляется по связи с НРД)", len(hb.Participants), at.Format("02.01.2006"))
|
||||
default:
|
||||
c.State, c.Detail = "ok", fmt.Sprintf("%d участников, от %s", len(hb.Participants), at.Format("02.01.2006"))
|
||||
}
|
||||
return c
|
||||
})
|
||||
run(func() ReadyCheck { return readyNetwork(ctx, s) })
|
||||
run(func() ReadyCheck {
|
||||
c := ReadyCheck{Order: 15, Title: "Учётные записи", Link: "/admin/setup#users"}
|
||||
var admins, others int
|
||||
for _, u := range globalUsers.List() {
|
||||
if !u.Active {
|
||||
continue
|
||||
}
|
||||
if u.Role == "admin" {
|
||||
admins++
|
||||
} else {
|
||||
others++
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case s.Auth.ADEnabled:
|
||||
c.State, c.Detail = "ok", fmt.Sprintf("вход через Active Directory; локальных: администраторов %d, других %d", admins, others)
|
||||
case others == 0:
|
||||
c.State, c.Detail = "warn", fmt.Sprintf("только администратор (%d) — заведите учётные записи операторов: у каждого свой вход, в журнале видно, кто что сделал", admins)
|
||||
default:
|
||||
c.State, c.Detail = "ok", fmt.Sprintf("администраторов %d, других пользователей %d", admins, others)
|
||||
}
|
||||
return c
|
||||
})
|
||||
wg.Wait()
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Order < out[j].Order })
|
||||
return out
|
||||
}
|
||||
|
||||
// readyNetwork — сетевые доступы, без которых обмен не заработает: контур НРД (ИШ ходит туда сам по ГОСТ-TLS —
|
||||
// здесь только TCP), справочник сертификатов Мосбиржи (LDAP) и сервер лицензий (ключ, продление, обновления).
|
||||
func readyNetwork(ctx context.Context, s Settings) ReadyCheck {
|
||||
c := ReadyCheck{Order: 14, Title: "Сетевые доступы", Link: "/admin/setup#server"}
|
||||
type target struct{ name, addr string }
|
||||
var ts []target
|
||||
switch contourOf(s.NSD.Profile) {
|
||||
case "prod":
|
||||
if strings.HasSuffix(s.NSD.Profile, "rsa") {
|
||||
ts = append(ts, target{"НРД (боевой, RSA)", "edor.nsd.ru:443"}, target{"справочник Мосбиржи (LDAP)", "vcert.pki.moex.com:50003"})
|
||||
} else {
|
||||
ts = append(ts, target{"НРД (боевой, ГОСТ)", "edog.nsd.ru:443"}, target{"справочник Мосбиржи (LDAP)", "vcert.pki.moex.com:50001"})
|
||||
}
|
||||
default:
|
||||
if strings.HasSuffix(s.NSD.Profile, "rsa") {
|
||||
ts = append(ts, target{"НРД (TEST3, RSA)", "rsa-t3.nsd.ru:443"}, target{"справочник Мосбиржи (LDAP)", "vcert.pki.moex.com:50007"})
|
||||
} else {
|
||||
ts = append(ts, target{"НРД (TEST3, ГОСТ)", "gost-t3.nsd.ru:443"}, target{"справочник Мосбиржи (LDAP)", "vcert.pki.moex.com:50005"})
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(s.Proxy.Address) == "" { // через прокси прямого TCP не будет — сервер лицензий проверяет его работа
|
||||
ts = append(ts, target{"сервер лицензий", "lic.zetit.ru:443"})
|
||||
}
|
||||
var bad, good []string
|
||||
var mu sync.Mutex
|
||||
var wg sync.WaitGroup
|
||||
for _, t := range ts {
|
||||
wg.Add(1)
|
||||
go func(t target) {
|
||||
defer wg.Done()
|
||||
d := net.Dialer{Timeout: 4 * time.Second}
|
||||
conn, err := d.DialContext(ctx, "tcp", t.addr)
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if err != nil {
|
||||
bad = append(bad, t.name+" ("+t.addr+")")
|
||||
return
|
||||
}
|
||||
conn.Close()
|
||||
good = append(good, t.name)
|
||||
}(t)
|
||||
}
|
||||
wg.Wait()
|
||||
sort.Strings(bad)
|
||||
if len(bad) > 0 {
|
||||
c.State, c.Detail = "err", "нет доступа: "+strings.Join(bad, ", ")+" — откройте эти адреса на межсетевом экране организации"
|
||||
return c
|
||||
}
|
||||
c.State, c.Detail = "ok", "доступны: "+strings.Join(good, ", ")
|
||||
return c
|
||||
}
|
||||
|
||||
func readyDepo(ctx context.Context, rc *RuntimeConfig) ReadyCheck {
|
||||
f := rc.Snapshot().Fancy
|
||||
c := ReadyCheck{Order: 3, Title: "Учёт DEPO", Link: "/admin/wizard?step=3&sub=done", Required: true}
|
||||
@@ -273,6 +375,19 @@ func readyDepo(ctx context.Context, rc *RuntimeConfig) ReadyCheck {
|
||||
return c
|
||||
}
|
||||
|
||||
// lastReadiness — итог последней проверки готовности: по нему отмечается шаг 10 мастера.
|
||||
var lastReadiness struct {
|
||||
sync.Mutex
|
||||
ready bool
|
||||
at time.Time
|
||||
}
|
||||
|
||||
func readinessDone() bool {
|
||||
lastReadiness.Lock()
|
||||
defer lastReadiness.Unlock()
|
||||
return lastReadiness.ready && time.Since(lastReadiness.at) < 24*time.Hour
|
||||
}
|
||||
|
||||
func registerReadiness(mux *http.ServeMux, rc *RuntimeConfig) {
|
||||
mux.HandleFunc("/admin/setup/readiness", func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||||
@@ -284,6 +399,9 @@ func registerReadiness(mux *http.ServeMux, rc *RuntimeConfig) {
|
||||
ready = false
|
||||
}
|
||||
}
|
||||
lastReadiness.Lock()
|
||||
lastReadiness.ready, lastReadiness.at = ready, time.Now()
|
||||
lastReadiness.Unlock()
|
||||
jsonOut(w, map[string]any{"checks": checks, "ready": ready, "at": time.Now().Format("02.01.2006 15:04:05")})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ type Settings struct {
|
||||
Backup BackupSettings `json:"backup,omitempty"`
|
||||
HTTPS HTTPSSettings `json:"https,omitempty"`
|
||||
Time TimeSettings `json:"time,omitempty"`
|
||||
Proxy ProxySettings `json:"proxy,omitempty"`
|
||||
LastTest *TestRunResult `json:"last_test,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
@@ -480,6 +481,15 @@ func (r *RuntimeConfig) UpdateHTTPS(s HTTPSSettings) error {
|
||||
return r.save()
|
||||
}
|
||||
|
||||
// UpdateProxy сохраняет прокси для выхода в интернет.
|
||||
func (r *RuntimeConfig) UpdateProxy(s ProxySettings) error {
|
||||
r.mu.Lock()
|
||||
r.data.Proxy = s
|
||||
r.data.UpdatedAt = time.Now()
|
||||
r.mu.Unlock()
|
||||
return r.save()
|
||||
}
|
||||
|
||||
// UpdateTime сохраняет настройки синхронизации времени.
|
||||
func (r *RuntimeConfig) UpdateTime(s TimeSettings) error {
|
||||
r.mu.Lock()
|
||||
|
||||
@@ -214,13 +214,15 @@ func NewServer(cfg ServerConfig) (*Server, error) {
|
||||
registerOutgoing(mux, adminTpl, outDeps)
|
||||
registerManual(mux, rc, svc) // ручной ввод исходящих переводов по бумажным поручениям
|
||||
registerJournal(mux, adminTpl, svc, incomingStore)
|
||||
registerNSDHealth(mux, rc) // связь с НРД: шапка (json) и «Проверить связь сейчас»
|
||||
registerBackup(mux, rc) // резервное копирование и восстановление (backup.go)
|
||||
registerIgate(mux, rc) // интеграционный шлюз НРД: состояние и установка (igate.go)
|
||||
registerReadiness(mux, rc) // итоговая проверка готовности (мастер, шаг 10)
|
||||
registerQuikReady(mux, rc) // проверка модуля неторговых поручений QUIK (только чтение)
|
||||
registerHTTPS(mux, rc) // HTTPS веб-интерфейса: перенаправление, свой сертификат
|
||||
registerTime(mux, rc) // сервер времени (NTP)
|
||||
registerNSDHealth(mux, rc) // связь с НРД: шапка (json) и «Проверить связь сейчас»
|
||||
registerBackup(mux, rc) // резервное копирование и восстановление (backup.go)
|
||||
registerIgate(mux, rc) // интеграционный шлюз НРД: состояние и установка (igate.go)
|
||||
registerReadiness(mux, rc) // итоговая проверка готовности (мастер, шаг 10)
|
||||
registerQuikReady(mux, rc) // проверка модуля неторговых поручений QUIK (только чтение)
|
||||
registerHTTPS(mux, rc) // HTTPS веб-интерфейса: перенаправление, свой сертификат
|
||||
registerTime(mux, rc) // сервер времени (NTP)
|
||||
registerProxy(mux, rc) // прокси для выхода в интернет
|
||||
applyProxy(rc)
|
||||
registerLKAPI(mux, rc, svc) // API для модуля личного кабинета клиента (bj-lk)
|
||||
registerHealth(mux)
|
||||
registerQuikOps(mux, rc, svc)
|
||||
|
||||
@@ -41,7 +41,7 @@ func validBody() string {
|
||||
"document": {"document_type": "21", "series": "4512", "number": "654321"}
|
||||
},
|
||||
"transferring_depository_inn": "0702345678",
|
||||
"receiving_depository_inn": "0710987654",
|
||||
"receiving_depository_inn": "7702015515",
|
||||
"cost_info": {"no": {}},
|
||||
"securities": [{
|
||||
"security_code": "MM0766162534",
|
||||
|
||||
@@ -82,12 +82,19 @@ func (s *Service) CreateClaim(ctx context.Context, in CreateClaimRequest) (Creat
|
||||
// ReferenceID, реквизиты) БЕЗ отправки и без создания заявки. Нужно, чтобы
|
||||
// сначала положить запрос в БД Fancy на проверку, а отправить позже.
|
||||
func (s *Service) BuildRequest(ctx context.Context, in CreateClaimRequest) (*m2m.M2MTransferRequest, error) {
|
||||
if s.defaultSender == "" {
|
||||
return nil, errors.New("организация не задана: укажите код участника НРД (мастер настройки, шаг «Организация») — без него перевод не сформировать")
|
||||
}
|
||||
in = withNSDSecCodes(in)
|
||||
domainClaim, err := dtoToClaim(in)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("lkgateway: dtoToClaim: %w", err)
|
||||
}
|
||||
receiver := s.receiverFor(in)
|
||||
if in.ReceiverCodeOverride == "" && handbookCodeByINN(in.ReceivingDepositoryINN) == "" && handbookLoaded() && !devMode(globalRC) {
|
||||
// запасной код получателя из настроек — чужой участник: НРД отклонит перевод (M2M15). Лучше сказать сразу.
|
||||
return nil, fmt.Errorf("%w: ИНН %s", errReceiverUnknown, in.ReceivingDepositoryINN)
|
||||
}
|
||||
req, err := m2mcore.EnrichRequest(ctx, s.store, domainClaim, m2mcore.SenderReceiver{
|
||||
SenderCode: s.defaultSender,
|
||||
ReceiverCode: receiver,
|
||||
@@ -98,6 +105,15 @@ func (s *Service) BuildRequest(ctx context.Context, in CreateClaimRequest) (*m2m
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// errReceiverUnknown — депозитария получателя нет в справочнике участников M2M НРД.
|
||||
var errReceiverUnknown = errors.New("депозитарий получателя не найден в справочнике участников M2M НРД")
|
||||
|
||||
// handbookLoaded — справочник участников M2M получен (без него получателя не определить ни для кого).
|
||||
func handbookLoaded() bool {
|
||||
raw, err := readHandbook()
|
||||
return err == nil && len(raw) > 200
|
||||
}
|
||||
|
||||
// receiverFor — код получателя (зачисляющего депозитария) в заголовке запроса: явный (робот НРД,
|
||||
// тест-кейсы) → по ИНН зачисляющего депозитария из справочника участников M2M → из настроек.
|
||||
// Без справочника заявки ЛК уходили с кодом из настроек, и НРД отвечал M2M15 (29.09.2026).
|
||||
|
||||
@@ -245,7 +245,7 @@ func (h *setupHandlers) renderWizard(w http.ResponseWriter, r *http.Request) {
|
||||
d.Done.Crypto = d.ValidataVer != "" || (s.Crypto.Provider != "" && s.Crypto.Provider != "stub" && s.Crypto.ModulePath != "")
|
||||
d.Done.Certs = len(ownValidCerts(d.Certs)) > 0
|
||||
d.Done.NSD = doneNSD(s)
|
||||
d.Done.TestRun = s.LastTest != nil || (!d.DevMode && d.Done.NSD) // вне режима разработки тестовой заявки нет
|
||||
d.Done.TestRun = s.LastTest != nil || (!d.DevMode && readinessDone()) // вне режима разработки — итоговая проверка готовности пройдена
|
||||
|
||||
// Мастер идёт по порядку: каждый шаг показывается, даже если он уже выполнен (установщиком,
|
||||
// лицензией) — на нём написано, что сделано, и пользователь сам нажимает «Далее». Без ?step
|
||||
@@ -332,7 +332,8 @@ func depoServerEdition(ctx context.Context, f FancyMSSQLSettings) (string, bool)
|
||||
}
|
||||
|
||||
// wizardSteps — шагов в мастере: 1 лицензия, 2 база bj, 3 учёт DEPO, 4 наша организация, 5 QUIK МНП,
|
||||
// 6 Валидата, 7 сертификаты, 8 шлюз НРД, 9 оповещения и резервные копии, 10 проверка готовности.
|
||||
// 6 Валидата, 7 шлюз НРД, 8 ключ и сертификаты (ключ ставится в профиль ИШ — после шлюза),
|
||||
// 9 оповещения и резервные копии, 10 проверка готовности.
|
||||
const wizardSteps = 10
|
||||
|
||||
// wizardProgressFile — номер последнего пройденного шага мастера (переход «Далее» на шаг N
|
||||
|
||||
@@ -59,8 +59,8 @@ func setupSteps(rc *RuntimeConfig) []SetupStep {
|
||||
}
|
||||
steps = append(steps,
|
||||
SetupStep{6, "Валидата", validata != "" || (s.Crypto.Provider != "" && s.Crypto.Provider != "stub" && s.Crypto.ModulePath != "")},
|
||||
SetupStep{7, "Сертификаты", certs > 0},
|
||||
SetupStep{8, "Шлюз НРД", doneNSD(s)},
|
||||
SetupStep{7, "Шлюз НРД", doneNSD(s)},
|
||||
SetupStep{8, "Ключ и сертификаты", certs > 0},
|
||||
SetupStep{9, "Оповещения и копии", doneAlerts(s)},
|
||||
)
|
||||
return steps
|
||||
|
||||
@@ -166,8 +166,46 @@ func httpsRedirect(rc *RuntimeConfig, next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
// selfSignedStale — самоподписанный сертификат выписан не на все нынешние адреса сервера (адрес сменили,
|
||||
// копию восстановили на другой машине): браузер ругался бы ещё и на имя — выпускаем заново.
|
||||
func selfSignedStale() bool {
|
||||
raw, err := os.ReadFile(tlsCertPath())
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
b, _ := pem.Decode(raw)
|
||||
if b == nil {
|
||||
return false
|
||||
}
|
||||
c, err := x509.ParseCertificate(b.Bytes)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_, ips := localAddrs()
|
||||
for _, ip := range ips {
|
||||
if ip.IsLoopback() || ip.To4() == nil {
|
||||
continue
|
||||
}
|
||||
found := false
|
||||
for _, have := range c.IPAddresses {
|
||||
if have.Equal(ip) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// startHTTPS — слушатель HTTPS рядом с основным HTTP. Ошибка не роняет bj: интерфейс остаётся по HTTP.
|
||||
func startHTTPS(rc *RuntimeConfig, handler http.Handler) *http.Server {
|
||||
if !rc.Snapshot().HTTPS.Custom && selfSignedStale() {
|
||||
log.Printf("https: адреса сервера изменились — новый самоподписанный сертификат")
|
||||
_ = ensureSelfSigned(true)
|
||||
}
|
||||
if err := ensureSelfSigned(false); err != nil {
|
||||
log.Printf("https: сертификат не создан: %v — HTTPS выключен", err)
|
||||
return nil
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
{{if devOn}}<form method="post" action="/admin/setup/test-nsd" style="margin:0">
|
||||
<input type="hidden" name="scenario" value="2001">
|
||||
<button type="submit" class="btn btn-ok">→ Отправить тестовый документ роботу</button>
|
||||
</form>{{else}}<a href="/admin/status" class="btn btn-ok">→ Проверить подключения</a>{{end}}
|
||||
</form>{{else}}<a href="/admin/wizard?step=8" class="btn btn-ok">→ Вернуться в мастер настройки</a> <a href="/admin/status" class="btn btn-secondary">Проверить подключения</a>{{end}}
|
||||
<form method="post" action="/admin/setup/keywizard/reset" style="margin:0">
|
||||
<button type="submit" class="btn btn-secondary">Установить ещё один ключ</button>
|
||||
</form>
|
||||
@@ -76,8 +76,10 @@
|
||||
|
||||
<div>
|
||||
<label style="font-weight:600;display:block;margin-bottom:6px">Для какого контура НРД этот ключ</label>
|
||||
<label style="display:flex;gap:8px;align-items:center;font-weight:400"><input type="radio" name="contour" value="test3" style="width:auto" {{if ne currentContour "prod"}}checked{{end}}> Тестовый TEST3 <span class="muted" style="font-size:12px">— сертификат от «Тестовый УЦ»</span></label>
|
||||
<label style="display:flex;gap:8px;align-items:center;font-weight:400"><input type="radio" name="contour" value="prod" style="width:auto" {{if eq currentContour "prod"}}checked{{end}}> Боевой <span class="muted" style="font-size:12px">— выдан УЦ Московской биржи для работы</span></label>
|
||||
{{$c := .SuggestContour}}{{if not $c}}{{$c = currentContour}}{{end}}
|
||||
{{if .SuggestIssuer}}<p class="muted" style="font-size:12px;margin:0 0 4px">В архиве сертификат от «{{.SuggestIssuer}}» — это {{if eq .SuggestContour "test3"}}тестовый{{else}}боевой{{end}} ключ.</p>{{end}}
|
||||
<label style="display:flex;gap:8px;align-items:center;font-weight:400"><input type="radio" name="contour" value="test3" style="width:auto" {{if ne $c "prod"}}checked{{end}}> Тестовый TEST3 <span class="muted" style="font-size:12px">— сертификат от «Тестовый УЦ»</span></label>
|
||||
<label style="display:flex;gap:8px;align-items:center;font-weight:400"><input type="radio" name="contour" value="prod" style="width:auto" {{if eq $c "prod"}}checked{{end}}> Боевой <span class="muted" style="font-size:12px">— выдан УЦ Московской биржи для работы</span></label>
|
||||
<span class="muted" style="font-size:12px">У тестового и боевого контуров разные ключи: ключ подключается только к каналу ИШ своего контура.</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -966,6 +966,19 @@
|
||||
<button type="submit" class="btn">Применить</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card" id="sec-proxy">
|
||||
<h2>Прокси для выхода в интернет</h2>
|
||||
<p class="muted">Если сервер выходит в интернет только через прокси: через него bj обращается к серверу лицензий (ключ продукта, продление, обновления), к сайту НРД (документы, дистрибутив ИШ). Адреса локальной сети — шлюз, учёт, модули — всегда напрямую. Пусто — без прокси.</p>
|
||||
<form method="post" action="/admin/setup/proxy" style="display:grid;gap:10px;max-width:640px">
|
||||
<div style="display:grid;grid-template-columns:2fr 1fr 1fr;gap:10px">
|
||||
<label style="margin:0">Адрес прокси<input name="address" value="{{.Settings.Proxy.Address}}" placeholder="http://proxy.example.ru:3128" style="width:100%;margin-top:4px"></label>
|
||||
<label style="margin:0">Логин<input name="user" value="{{.Settings.Proxy.User}}" autocomplete="off" style="width:100%;margin-top:4px"></label>
|
||||
<label style="margin:0">Пароль {{if .Settings.Proxy.Password}}<span class="muted">(задан)</span>{{end}}<input name="password" type="password" autocomplete="new-password" style="width:100%;margin-top:4px"></label>
|
||||
</div>
|
||||
<label style="margin:0">Ещё адреса напрямую (через запятую) <span class="muted">— локальная сеть и *.local уже напрямую</span><input name="no_proxy" value="{{.Settings.Proxy.NoProxy}}" placeholder="corp.example.ru" style="width:100%;margin-top:4px"></label>
|
||||
<div><button type="submit" class="btn">Сохранить и проверить</button></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card" id="sec-network">
|
||||
<h2>Сеть</h2>
|
||||
<p style="margin:6px 0">Сервер: <code>{{serverAddrs}}</code></p>
|
||||
|
||||
@@ -36,8 +36,8 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
|
||||
<a href="/admin/wizard?step=4" title="Перейти к шагу 4" class="wstep {{if .Done.Org}}done{{end}} {{if eq .Step 4}}current{{end}}"><span class="bub">{{if .Done.Org}}✓{{else}}4{{end}}</span><span class="lbl">Организация</span></a>
|
||||
<a href="/admin/wizard?step=5" title="Перейти к шагу 5" class="wstep {{if .Done.Quik}}done{{end}} {{if eq .Step 5}}current{{end}}"><span class="bub">{{if .Done.Quik}}✓{{else}}5{{end}}</span><span class="lbl">QUIK МНП</span></a>
|
||||
<a href="/admin/wizard?step=6" title="Перейти к шагу 6" class="wstep {{if .Done.Crypto}}done{{end}} {{if eq .Step 6}}current{{end}}"><span class="bub">{{if .Done.Crypto}}✓{{else}}6{{end}}</span><span class="lbl">Валидата</span></a>
|
||||
<a href="/admin/wizard?step=7" title="Перейти к шагу 7" class="wstep {{if .Done.Certs}}done{{end}} {{if eq .Step 7}}current{{end}}"><span class="bub">{{if .Done.Certs}}✓{{else}}7{{end}}</span><span class="lbl">Сертификаты</span></a>
|
||||
<a href="/admin/wizard?step=8" title="Перейти к шагу 8" class="wstep {{if .Done.NSD}}done{{end}} {{if eq .Step 8}}current{{end}}"><span class="bub">{{if .Done.NSD}}✓{{else}}8{{end}}</span><span class="lbl">Шлюз НРД</span></a>
|
||||
<a href="/admin/wizard?step=7" title="Перейти к шагу 7" class="wstep {{if .Done.NSD}}done{{end}} {{if eq .Step 7}}current{{end}}"><span class="bub">{{if .Done.NSD}}✓{{else}}7{{end}}</span><span class="lbl">Шлюз НРД</span></a>
|
||||
<a href="/admin/wizard?step=8" title="Перейти к шагу 8" class="wstep {{if .Done.Certs}}done{{end}} {{if eq .Step 8}}current{{end}}"><span class="bub">{{if .Done.Certs}}✓{{else}}8{{end}}</span><span class="lbl">Ключ</span></a>
|
||||
<a href="/admin/wizard?step=9" title="Перейти к шагу 9" class="wstep {{if .Done.Alerts}}done{{end}} {{if eq .Step 9}}current{{end}}"><span class="bub">{{if .Done.Alerts}}✓{{else}}9{{end}}</span><span class="lbl">Оповещения</span></a>
|
||||
<a href="/admin/wizard?step=10" title="Перейти к шагу 10" class="wstep {{if .Done.TestRun}}done{{end}} {{if eq .Step 10}}current{{end}}"><span class="bub">{{if .Done.TestRun}}✓{{else}}10{{end}}</span><span class="lbl">Проверка</span></a>
|
||||
</div>
|
||||
@@ -381,10 +381,10 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
|
||||
{{if eq .Step 6}}
|
||||
<div class="card">
|
||||
<h2><span class="dot {{if .Done.Crypto}}ok{{else}}err{{end}}"></span>Шаг 6. СКЗИ «Валидата Клиент L»</h2>
|
||||
<p>СКЗИ «Валидата Клиент L» проверяет подписи НРД на квитанциях и сообщениях и нужно для работы с ключами и сертификатами (шаг 7).</p>
|
||||
<p>СКЗИ «Валидата Клиент L» проверяет подписи НРД на квитанциях и сообщениях и нужно для работы с ключами и сертификатами (шаг 8).</p>
|
||||
{{if .ValidataVer}}
|
||||
<p>✓ Валидата установлена вместе с bj: версия {{.ValidataVer}}, каталог <code>/opt/Validata/VDCSP</code>. Ничего настраивать на этом шаге не нужно.</p>
|
||||
<p class="muted" style="font-size:13px">bj работает с Валидатой через свою службу криптографии <code>bj-crypto</code>{{if eq .CryptoSvc "active"}} — она запущена{{else}} — сейчас она не запущена: служба ставится обновлением с сервера лицензий (шаг 1, кнопка «Обновить сейчас»), а работает с ключом, установленным на шаге 7{{end}}.</p>
|
||||
<p class="muted" style="font-size:13px">bj работает с Валидатой через свою службу криптографии <code>bj-crypto</code>{{if eq .CryptoSvc "active"}} — она запущена{{else}} — сейчас она не запущена: служба ставится обновлением с сервера лицензий (шаг 1, кнопка «Обновить сейчас»), а работает с ключом, установленным на шаге 8{{end}}.</p>
|
||||
{{else}}
|
||||
<p style="color:var(--err)">Валидата на этом сервере не найдена.</p>
|
||||
<div class="help-block">
|
||||
@@ -403,11 +403,44 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{/* ============= ШАГ 7: Сертификаты ============= */}}
|
||||
{{/* ============= ШАГ 7: Шлюз НРД (до ключа: ключ ставится в профиль ИШ) ============= */}}
|
||||
{{if eq .Step 7}}
|
||||
<div class="card">
|
||||
<h2><span class="dot {{if .Done.Certs}}ok{{else}}err{{end}}"></span>Шаг 7. Сертификаты</h2>
|
||||
<h2><span class="dot {{if .Done.NSD}}ok{{else}}err{{end}}"></span>Шаг 7. Интеграционный шлюз НРД (ИШ)</h2>
|
||||
<div class="help-block">
|
||||
<strong>Что это.</strong> Интеграционный шлюз — программа НРД на этом же сервере: через неё bj отправляет пакеты M2M в НРД и получает ответы. bj работает со шлюзом по его адресу на сервере (обычно <code>http://localhost:8090</code>).<br>
|
||||
<strong>К кому обратиться.</strong> Дистрибутив ИШ выпускает НРД (<a href="https://www.nsd.ru/workflow/system/programs/" target="_blank" rel="noopener">nsd.ru/workflow/system/programs</a>). Доступ к контурам — сначала тестовому TEST3, затем боевому — НРД открывает по заявке: обратитесь к своему менеджеру НРД или в поддержку электронного документооборота.
|
||||
</div>
|
||||
{{template "igateInstall" .}}
|
||||
<details style="margin-top:8px"><summary style="cursor:pointer;color:var(--accent);font-size:13px">Дополнительно: адрес шлюза (обычно не нужно)</summary>
|
||||
<form method="post" action="/admin/setup/nsd" style="margin-top:10px;display:grid;gap:10px;max-width:680px">
|
||||
<input type="hidden" name="return_to" value="/admin/wizard?step=7">
|
||||
{{if .Settings.NSD.IGWv904}}<input type="hidden" name="igw_v904" value="on">{{end}}
|
||||
<input type="hidden" name="keep_requisites" value="1">
|
||||
<input type="hidden" name="profile" value="{{if .Settings.NSD.Profile}}{{.Settings.NSD.Profile}}{{else}}test3-gost{{end}}">
|
||||
<label style="margin:0">Адрес шлюза на этом сервере
|
||||
<input type="text" name="igw_base_url" value="{{if .Settings.NSD.IGWBaseURL}}{{.Settings.NSD.IGWBaseURL}}{{else}}http://localhost:8090{{end}}" placeholder="http://localhost:8090" style="padding:8px;width:100%;box-sizing:border-box;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px;margin-top:4px"></label>
|
||||
<p id="igw-onyx" style="display:none;color:var(--err);font-size:13px;margin:0">Это адрес веб-сервиса НРД (ONYX), а не шлюза: bj работает через ИШ на этом сервере. Укажите адрес шлюза — обычно <a href="#" id="igw-def">http://localhost:8090</a>.</p>
|
||||
<script>(function(){var u=document.querySelector('[name=igw_base_url]'),w=document.getElementById('igw-onyx');
|
||||
function chk(){w.style.display=/onyx|nsd\.ru/i.test(u.value)?'block':'none'} u.addEventListener('input',chk); chk();
|
||||
document.getElementById('igw-def').onclick=function(e){e.preventDefault();u.value='http://localhost:8090';chk()};})();</script>
|
||||
<div><button type="submit" class="btn">Сохранить и проверить</button></div>
|
||||
</form>
|
||||
</details>
|
||||
<p class="muted" style="font-size:13px;margin-top:12px">Депозитарные реквизиты в НРД: {{if and .Settings.NSD.DeponentCode .Settings.NSD.AccountID .Settings.NSD.SectionID}}код <code>{{.Settings.NSD.DeponentCode}}</code>, счёт <code>{{.Settings.NSD.AccountID}}</code>, раздел <code>{{.Settings.NSD.SectionID}}</code>{{else}}<span style="color:var(--err)">не заданы</span>{{end}} — на <a href="/admin/wizard?step=4#nsd-req">шаге 4</a>.</p>
|
||||
<div style="margin-top:20px;display:flex;justify-content:space-between">
|
||||
<a href="/admin/wizard?step=6" class="btn" style="background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8);text-decoration:none">← К шагу 6</a>
|
||||
{{if .Done.NSD}}<a href="/admin/wizard?step=8" class="btn" style="text-decoration:none">К шагу 8: ключ →</a>{{else}}<a href="/admin/wizard?step=8&skip=nsd" class="btn" style="background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8);text-decoration:none">Пропустить ({{if devOn}}mock-режим{{else}}обмен с НРД не настроен{{end}}) →</a>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{/* ============= ШАГ 8: Ключ и сертификаты (после шлюза: ключ ставится в профиль ИШ) ============= */}}
|
||||
{{if eq .Step 8}}
|
||||
<div class="card">
|
||||
<h2><span class="dot {{if .Done.Certs}}ok{{else}}err{{end}}"></span>Шаг 8. Ключ и сертификаты</h2>
|
||||
<p>Здесь две части: ключ и сертификат вашей организации (ставите вы) и сертификаты НРД и удостоверяющих центров (bj ведёт их сам).</p>
|
||||
{{if not .Done.NSD}}<p style="padding:8px 12px;border-radius:8px;background:var(--accent-weak);margin:8px 0">Ключ устанавливается в профиль интеграционного шлюза — сначала установите шлюз НРД на <a href="/admin/wizard?step=7">шаге 7</a>.</p>{{end}}
|
||||
|
||||
<h3 style="margin-top:14px">1. Ключ и сертификат организации</h3>
|
||||
<div class="help-block"><strong>Что это.</strong> Ключ электронной подписи вашей организации для обмена с НРД и его сертификат.<br>
|
||||
@@ -423,7 +456,7 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
|
||||
<strong>Откуда берутся.</strong> НРД и Московская биржа публикуют сертификаты в постоянных местах: комплект ГОСТ МБ, НКЦ и НРД — <code>https://fs.moex.com/cdp/sert/GOST.zip</code>, сертификаты УЦ — <code>https://fs.moex.com/cdp/</code>. Когда там появляется новый сертификат, мы добавляем его в комплект bj, и он приезжает к вам с обновлением.<br>
|
||||
Состояние, ручная проверка и выключение — «Настройка → Криптография → Сертификаты НРД в справочнике ИШ».
|
||||
</div>
|
||||
{{if .Settings.NSDCerts.LastRows}}<p class="muted" style="font-size:13px">Последняя сверка: {{.Settings.NSDCerts.LastRun.Format "02.01.2006 15:04"}}{{if .Settings.NSDCerts.LastProfile}}, профиль ИШ {{.Settings.NSDCerts.LastProfile}}{{end}}.</p>{{else}}<p class="muted" style="font-size:13px">Первая сверка пройдёт, когда будут настроены ключ и шлюз НРД (шаг 8).</p>{{end}}
|
||||
{{if .Settings.NSDCerts.LastRows}}<p class="muted" style="font-size:13px">Последняя сверка: {{.Settings.NSDCerts.LastRun.Format "02.01.2006 15:04"}}{{if .Settings.NSDCerts.LastProfile}}, профиль ИШ {{.Settings.NSDCerts.LastProfile}}{{end}}.</p>{{else}}<p class="muted" style="font-size:13px">Первая сверка пройдёт сразу после установки ключа (шлюз НРД — шаг 7).</p>{{end}}
|
||||
|
||||
{{if .Certs}}
|
||||
<h3 style="margin-top:18px">Установленные сертификаты ({{len .Certs}})</h3>
|
||||
@@ -445,41 +478,9 @@ a.wstep:hover .bub { box-shadow:0 0 0 4px var(--accent-weak); }
|
||||
<p class="muted" style="margin-top:12px">Ключ организации ещё не установлен. Можно пропустить этот шаг и вернуться к нему, когда будет архив ключа.</p>
|
||||
{{end}}
|
||||
|
||||
<div style="margin-top:20px;display:flex;justify-content:space-between">
|
||||
<a href="/admin/wizard?step=6" class="btn" style="background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8);text-decoration:none">← К шагу 6</a>
|
||||
<a href="/admin/wizard?step=8" class="btn" style="text-decoration:none">К шагу 8 →</a>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{/* ============= ШАГ 8: Шлюз НРД ============= */}}
|
||||
{{if eq .Step 8}}
|
||||
<div class="card">
|
||||
<h2><span class="dot {{if .Done.NSD}}ok{{else}}err{{end}}"></span>Шаг 8. Интеграционный шлюз НРД (ИШ)</h2>
|
||||
<div class="help-block">
|
||||
<strong>Что это.</strong> Интеграционный шлюз — программа НРД на этом же сервере: через неё bj отправляет пакеты M2M в НРД и получает ответы. bj работает со шлюзом по его адресу на сервере (обычно <code>http://localhost:8090</code>).<br>
|
||||
<strong>К кому обратиться.</strong> Дистрибутив ИШ выпускает НРД (<a href="https://www.nsd.ru/workflow/system/programs/" target="_blank" rel="noopener">nsd.ru/workflow/system/programs</a>). Доступ к контурам — сначала тестовому TEST3, затем боевому — НРД открывает по заявке: обратитесь к своему менеджеру НРД или в поддержку электронного документооборота.
|
||||
</div>
|
||||
{{template "igateInstall" .}}
|
||||
<details style="margin-top:8px"><summary style="cursor:pointer;color:var(--accent);font-size:13px">Дополнительно: адрес шлюза (обычно не нужно)</summary>
|
||||
<form method="post" action="/admin/setup/nsd" style="margin-top:10px;display:grid;gap:10px;max-width:680px">
|
||||
<input type="hidden" name="return_to" value="/admin/wizard?step=8">
|
||||
{{if .Settings.NSD.IGWv904}}<input type="hidden" name="igw_v904" value="on">{{end}}
|
||||
<input type="hidden" name="keep_requisites" value="1">
|
||||
<input type="hidden" name="profile" value="{{if .Settings.NSD.Profile}}{{.Settings.NSD.Profile}}{{else}}test3-gost{{end}}">
|
||||
<label style="margin:0">Адрес шлюза на этом сервере
|
||||
<input type="text" name="igw_base_url" value="{{if .Settings.NSD.IGWBaseURL}}{{.Settings.NSD.IGWBaseURL}}{{else}}http://localhost:8090{{end}}" placeholder="http://localhost:8090" style="padding:8px;width:100%;box-sizing:border-box;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px;margin-top:4px"></label>
|
||||
<p id="igw-onyx" style="display:none;color:var(--err);font-size:13px;margin:0">Это адрес веб-сервиса НРД (ONYX), а не шлюза: bj работает через ИШ на этом сервере. Укажите адрес шлюза — обычно <a href="#" id="igw-def">http://localhost:8090</a>.</p>
|
||||
<script>(function(){var u=document.querySelector('[name=igw_base_url]'),w=document.getElementById('igw-onyx');
|
||||
function chk(){w.style.display=/onyx|nsd\.ru/i.test(u.value)?'block':'none'} u.addEventListener('input',chk); chk();
|
||||
document.getElementById('igw-def').onclick=function(e){e.preventDefault();u.value='http://localhost:8090';chk()};})();</script>
|
||||
<div><button type="submit" class="btn">Сохранить и проверить</button></div>
|
||||
</form>
|
||||
</details>
|
||||
<p class="muted" style="font-size:13px;margin-top:12px">Депозитарные реквизиты в НРД: {{if and .Settings.NSD.DeponentCode .Settings.NSD.AccountID .Settings.NSD.SectionID}}код <code>{{.Settings.NSD.DeponentCode}}</code>, счёт <code>{{.Settings.NSD.AccountID}}</code>, раздел <code>{{.Settings.NSD.SectionID}}</code>{{else}}<span style="color:var(--err)">не заданы</span>{{end}} — на <a href="/admin/wizard?step=4#nsd-req">шаге 4</a>.</p>
|
||||
<div style="margin-top:20px;display:flex;justify-content:space-between">
|
||||
<a href="/admin/wizard?step=7" class="btn" style="background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8);text-decoration:none">← К шагу 7</a>
|
||||
{{if .Done.NSD}}<a href="/admin/wizard?step=9" class="btn" style="text-decoration:none">К шагу 9 →</a>{{else}}<a href="/admin/wizard?step=9&skip=nsd" class="btn" style="background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8);text-decoration:none">Пропустить ({{if devOn}}mock-режим{{else}}обмен с НРД не настроен{{end}}) →</a>{{end}}
|
||||
<a href="/admin/wizard?step=9" class="btn" style="text-decoration:none">К шагу 9 →</a>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user