Files
Bridge-and-Join-s/internal/lkgateway/lkfx.go
T
zuevav 7ce4c2c79f Защита рецептов: SQL DEPO/SPECTRE/QUIK вынесен в пакет fsql под garble
Реализация варианта B из аудита защиты знаний. Прямой garble всего bj-server
ломает html/template (он обращается к полям структур по имени, garble их
переименовывает). Решение — селективная обфускация: GOGARBLE обфусцирует ТОЛЬКО
пакет с секретами, шаблоны и обработчики не затрагиваются.

95 SQL-запросов к DEPO/SPECTRE/QUIK и трансферным таблицам FTV_M2M вынесены из
lkgateway в отдельный пакет internal/lkgateway/fsql как переменные (именно var,
не const: const компилятор вставил бы обратно в необфусцируемый пакет, и
шифрование бы не сработало — проверено). Сами обработчики, типы и параметры
sql.Named остались на месте; пакет держит только строки запросов.

build-release.sh собирает bj-server с `GOGARBLE=.../fsql garble -literals`
(нужен отдельный SDK go1.26 — модуль-кэшный тулчейн garble не подходит); если
garble/go1.26 не найдены — обычная сборка с предупреждением.

Проверено на реальном garble-бинаре (запускается, /admin и страницы на
структурах работают): рецепт чтения чужих систем из бинаря исчез —
OD_RESTS 13→0, OD_WIRING 5→0, OD_TURNS 1→0, OD_BALANCES 3→0, D_B_CONTRACTS 1→0,
OD_CLIENTS_PORTS 10→0, DepoLimits/MoneyLimits →0. Остаток FTV_M2M — упоминания
имён таблиц в операторских embed-шаблонах и логах, не сам рецепт.

Стенд собран обычной сборкой (без garble) — трассировки остаются читаемыми.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
2026-09-18 20:29:47 +03:00

127 lines
4.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package lkgateway
// lkfx.go — курсы валют к рублю для оценки валютных выпусков.
//
// Номинал «замещающих» и еврооблигаций в экспорте QUIK задан в валюте (FaceUnit = USD, EUR,
// CNY), а цена — в процентах от него. Чтобы показать стоимость в рублях, нужен курс. В базе
// экспорта курсов нет (валютный рынок не выгружается), поэтому берём официальный курс из
// учётной системы: OD_VALUES_RATES с MARKET = 0 — сначала DEPO, затем SPECTRE, и берём более
// свежий. Дата курса показывается клиенту: курс может отставать, и выдавать оценку за
// сегодняшнюю нельзя.
import (
"context"
"database/sql"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway/fsql"
"strings"
"sync"
"time"
)
// LKFxRate — курс валюты к рублю и дата, на которую он установлен.
type LKFxRate struct {
Rate float64
At time.Time
}
var fxCache = struct {
mu sync.Mutex
at time.Time
m map[string]LKFxRate
}{}
// lkFxRates — курсы валют для оценки (ключ — код валюты в верхнем регистре).
// Источник по умолчанию — Банк России (см. cbrfx.go); если интернета нет или
// в настройках выбрана учётная система, берём OD_VALUES_RATES, а недостающие
// валюты дополняем последним известным курсом из lk.fx_rates.
func lkFxRates(ctx context.Context, rc *RuntimeConfig) map[string]LKFxRate {
set := rc.Snapshot()
out := map[string]LKFxRate{}
if !strings.EqualFold(strings.TrimSpace(set.LK.FXSource), "accounting") {
if cbr, cerr := cbrToday(ctx, rc); len(cbr) > 0 {
return cbr
} else if cerr != "" {
// молча уходим на запасные источники: кабинет не должен падать
// из-за недоступного сайта ЦБ
_ = cerr
}
}
for code, r := range lkFxAccounting(ctx, set.Fancy) {
out[code] = r
}
for code, r := range lkFxLoad(ctx, rc, time.Time{}) {
if old, ok := out[code]; !ok || r.At.After(old.At) {
out[code] = r
}
}
return out
}
// lkFxAccounting — курсы из учётной системы (OD_VALUES_RATES, MARKET = 0), кэш на час.
func lkFxAccounting(ctx context.Context, s FancyMSSQLSettings) map[string]LKFxRate {
fxCache.mu.Lock()
if fxCache.m != nil && time.Since(fxCache.at) < time.Hour {
m := fxCache.m
fxCache.mu.Unlock()
return m
}
fxCache.mu.Unlock()
out := map[string]LKFxRate{}
if strings.TrimSpace(s.Server) == "" {
return out
}
for _, db := range []string{s.DepoDB(), s.SpectreDB()} {
conn, err := fancyOpen(s, db)
if err != nil {
continue
}
cctx, cancel := context.WithTimeout(ctx, 15*time.Second)
rows, err := conn.QueryContext(cctx, fsql.Fx01)
if err == nil {
for rows.Next() {
var code string
var at sql.NullTime
var rate float64
if rows.Scan(&code, &at, &rate) != nil || rate <= 0 {
continue
}
cur := LKFxRate{Rate: rate}
if at.Valid {
cur.At = at.Time
}
// из двух баз берём курс посвежее
if old, ok := out[code]; !ok || cur.At.After(old.At) {
out[code] = cur
}
}
rows.Close()
}
cancel()
conn.Close()
}
fxCache.mu.Lock()
fxCache.m, fxCache.at = out, time.Now()
fxCache.mu.Unlock()
return out
}
// lkFxFor — курс валюты к рублю: рубль — единица, неизвестная валюта — 0 (оценивать нельзя).
func lkFxFor(rates map[string]LKFxRate, cur string) LKFxRate {
c := strings.ToUpper(strings.TrimSpace(cur))
switch c {
case "", "SUR", "RUB", "RUR":
return LKFxRate{Rate: 1}
}
return rates[c]
}
// lkFxReset — сбросить кэши курсов (после смены источника или ручного обновления).
func lkFxReset() {
fxCache.mu.Lock()
fxCache.m, fxCache.at = nil, time.Time{}
fxCache.mu.Unlock()
cbrCache.mu.Lock()
cbrCache.at, cbrCache.err = time.Time{}, ""
cbrCache.mu.Unlock()
}