HTTPS: http:// перебрасывается на https://; имя сервера — с проверкой DNS; скрипты DEPO при обновлении
Порт HTTPS принимает и http://: 308 на https:// того же имени и порта (было «Client sent an HTTP request to an HTTPS server»). Доменное имя сервера сохраняется, только если оно есть в DNS и указывает на этот сервер; иначе — что добавить/исправить (запись A → IP сервера). Сообщение об успехе — с адресом https://имя:порт/ и честно про самоподписанный сертификат. Обновление bj с новыми процедурами DEPO: отпечаток установленного набора (depo_procs_applied) не совпадает со встроенным — на главной и в настройках плашка «требуются повышенные права на базу данных» с вводом логина/пароля администратора SQL (не сохраняются) и «Применить скрипты». Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
This commit is contained in:
+18
-14
@@ -140,20 +140,24 @@ var templateFuncs = template.FuncMap{
|
||||
}
|
||||
return "IP-сервера"
|
||||
},
|
||||
"timeSynced": func() bool { ok, _ := timeStatus(context.Background()); return ok },
|
||||
"serverTimeZone": serverTimeZone,
|
||||
"letter": letterView,
|
||||
"mailtoLink": mailtoLink,
|
||||
"handbookDepos": handbookDepos,
|
||||
"timeZones": func() any { return wizTimeZones },
|
||||
"timeZoneKnown": timeZoneKnown,
|
||||
"clientVer": clientVersion,
|
||||
"verTitle": versionTitle,
|
||||
"contourRu": contourRu,
|
||||
"looksErr": looksErr,
|
||||
"intList": func(v ...int) []int { return v },
|
||||
"nsdSegTip": nsdSegTip,
|
||||
"nsdSegPop": nsdSegPop,
|
||||
"timeSynced": func() bool { ok, _ := timeStatus(context.Background()); return ok },
|
||||
"serverTimeZone": serverTimeZone,
|
||||
"letter": letterView,
|
||||
"mailtoLink": mailtoLink,
|
||||
"handbookDepos": handbookDepos,
|
||||
"timeZones": func() any { return wizTimeZones },
|
||||
"timeZoneKnown": timeZoneKnown,
|
||||
"clientVer": clientVersion,
|
||||
"verTitle": versionTitle,
|
||||
"contourRu": contourRu,
|
||||
"looksErr": looksErr,
|
||||
"intList": func(v ...int) []int { return v },
|
||||
"nsdSegTip": nsdSegTip,
|
||||
"serverIPs": serverIPs,
|
||||
"httpsPort": func() int { return globalRC.Snapshot().HTTPS.port() },
|
||||
"depoProcsPending": depoProcsPending,
|
||||
"depoProcOrder": func() string { return depoProcOrderText("→") },
|
||||
"nsdSegPop": nsdSegPop,
|
||||
// participantOK / participantBanner — анкета участника M2M принята НРД; плашка наверху раздела НРД, пока нет.
|
||||
"participantOK": participantAccepted,
|
||||
"participantBanner": participantBanner,
|
||||
|
||||
@@ -10,8 +10,10 @@ package lkgateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"embed"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -85,6 +87,28 @@ func depoProcOrderText(sep string) string {
|
||||
return strings.Join(parts, sep)
|
||||
}
|
||||
|
||||
// depoProcsHash — отпечаток набора процедур, который bj установит (с учётом правки под клиента).
|
||||
func depoProcsHash(f FancyMSSQLSettings) string {
|
||||
h := sha256.New()
|
||||
for _, p := range depoProcInstallOrder {
|
||||
h.Write([]byte(p.Name + "\n" + strings.TrimSpace(depoProcEffective(f, p.Name)) + "\n"))
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil))[:16]
|
||||
}
|
||||
|
||||
// depoProcsPending — обновление bj принесло процедуры DEPO, которые ещё не применены в базе: bj работает с
|
||||
// DEPO (подключение есть, обработка включена), а отпечаток установленного набора не совпадает со встроенным.
|
||||
func depoProcsPending() bool {
|
||||
if globalRC == nil {
|
||||
return false
|
||||
}
|
||||
f := globalRC.Snapshot().Fancy
|
||||
if strings.TrimSpace(f.Server) == "" || (!f.DepoJobRun && f.DepoJobMode == "") {
|
||||
return false
|
||||
}
|
||||
return f.DepoProcsApplied != depoProcsHash(f)
|
||||
}
|
||||
|
||||
// depoProcList — порядок процедур для шаблона (редактор текстов).
|
||||
func depoProcList() []struct{ Name, File string } { return depoProcInstallOrder }
|
||||
|
||||
@@ -315,6 +339,7 @@ func (h *setupHandlers) installFancyDepoProcs(w http.ResponseWriter, r *http.Req
|
||||
pmsg, pok := depoProcCheck(ctx, h.rc)
|
||||
s = h.rc.Snapshot().Fancy
|
||||
s.DepoProcCheck, s.DepoProcCheckOK, s.DepoProcCheckAt = pmsg, pok, time.Now()
|
||||
s.DepoProcsApplied, s.DepoProcsAppliedAt = depoProcsHash(s), time.Now() // применено — плашка обновления уходит
|
||||
_ = h.rc.UpdateFancy(s)
|
||||
setupFlash(w, r, "Процедуры DEPO: "+msg+". Проверка обработки: "+pmsg)
|
||||
}
|
||||
|
||||
@@ -217,6 +217,10 @@ type FancyMSSQLSettings struct {
|
||||
// AccountPrefix — начало номера торгового счёта клиента в OD_CLIENTS (у Фонтвьель «ВТНКЦ»): по нему bj
|
||||
// находит счёт инвестора при сверке переводов, в цикле QUIK и при ручном вводе. Пусто — «ВТНКЦ».
|
||||
AccountPrefix string `json:"account_prefix,omitempty"`
|
||||
// DepoProcsApplied — отпечаток набора процедур bj2depo, установленного в DEPO из bj (depo_procs.go):
|
||||
// отличается от встроенного — обновление bj принесло новые процедуры, их нужно применить правами администратора БД.
|
||||
DepoProcsApplied string `json:"depo_procs_applied,omitempty"`
|
||||
DepoProcsAppliedAt time.Time `json:"depo_procs_applied_at,omitempty"`
|
||||
|
||||
// Обработка трансферных таблиц DEPO силами bj (вместо джобы SQL Agent bj2depo):
|
||||
// bj сам вызывает процедуру DepoJobProc каждые DepoJobEverySec секунд.
|
||||
|
||||
@@ -398,6 +398,32 @@ func registerHostname(mux *http.ServeMux, rc *RuntimeConfig) {
|
||||
names = append(names, f)
|
||||
}
|
||||
names = uniqStrings(names)
|
||||
// имя должно быть в DNS и указывать на этот сервер — иначе браузер всё равно не откроет bj по нему
|
||||
mine := map[string]bool{}
|
||||
for _, ip := range serverIPs() {
|
||||
mine[ip] = true
|
||||
}
|
||||
var resolved []string
|
||||
for _, n := range names {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 4*time.Second)
|
||||
addrs, err := net.DefaultResolver.LookupHost(ctx, n)
|
||||
cancel()
|
||||
if err != nil || len(addrs) == 0 {
|
||||
setupFlash(w, r, "Имя «"+n+"» не найдено в DNS. Попросите администратора сети добавить на DNS-сервере (контроллере домена) запись A: "+
|
||||
n+" → "+strings.Join(serverIPs(), " или ")+" — и нажмите «Сохранить» ещё раз.")
|
||||
return
|
||||
}
|
||||
ok := false
|
||||
for _, a := range addrs {
|
||||
ok = ok || mine[a]
|
||||
}
|
||||
if !ok && len(mine) > 0 {
|
||||
setupFlash(w, r, "Имя «"+n+"» в DNS указывает на "+strings.Join(addrs, ", ")+", а у сервера bj адрес "+strings.Join(serverIPs(), ", ")+
|
||||
". Исправьте запись A на DNS-сервере и нажмите «Сохранить» ещё раз.")
|
||||
return
|
||||
}
|
||||
resolved = append(resolved, n+" → "+strings.Join(addrs, ", "))
|
||||
}
|
||||
h := rc.Snapshot().HTTPS
|
||||
h.Hostnames = names
|
||||
if err := rc.UpdateHTTPS(h); err != nil {
|
||||
@@ -416,7 +442,8 @@ func registerHostname(mux *http.ServeMux, rc *RuntimeConfig) {
|
||||
setupFlash(w, r, "Доменные имена убраны. Сертификат перевыпущен на имя хоста и IP.")
|
||||
return
|
||||
}
|
||||
setupFlash(w, r, "Готово: "+strings.Join(names, ", ")+" вписаны в сертификат. Теперь на вашем DNS "+
|
||||
"(контроллере домена) направьте эти имена на сервер bj — и интерфейс откроется по ним без предупреждения браузера.")
|
||||
setupFlash(w, r, "Готово: "+strings.Join(resolved, "; ")+" — имя есть в DNS и вписано в сертификат. Адрес bj: https://"+names[0]+":"+
|
||||
strconv.Itoa(h.port())+"/ (http:// перебросит на https:// сам). Сертификат самоподписанный — браузер один раз попросит подтвердить исключение; "+
|
||||
"без предупреждения — после загрузки сертификата организации («Сертификат организации» ниже).")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ package lkgateway
|
||||
// ходят по HTTP на localhost и не перенаправляются.
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
@@ -245,6 +246,7 @@ func startHTTPS(rc *RuntimeConfig, handler http.Handler) *http.Server {
|
||||
return nil
|
||||
}
|
||||
httpsUp.Store(true)
|
||||
ln = newHTTPSSniffListener(ln, rc.Snapshot().HTTPS.port())
|
||||
go func() {
|
||||
log.Printf("lk-gateway: listen https %s", srv.Addr)
|
||||
if err := srv.ServeTLS(ln, "", ""); err != nil && err != http.ErrServerClosed {
|
||||
@@ -326,3 +328,87 @@ func registerHTTPS(mux *http.ServeMux, rc *RuntimeConfig) {
|
||||
setupFlash(w, r, "HTTPS: создан новый самоподписанный сертификат на имя сервера и его адреса")
|
||||
})
|
||||
}
|
||||
|
||||
// httpsSniffListener — порт HTTPS принимает и обычный http://: такой запрос перенаправляется (308) на
|
||||
// https:// того же имени и порта, вместо ответа Go «Client sent an HTTP request to an HTTPS server».
|
||||
// TLS-соединение узнаётся по первому байту (0x16 — handshake) и идёт дальше без изменений.
|
||||
type httpsSniffListener struct {
|
||||
net.Listener
|
||||
conns chan net.Conn
|
||||
errs chan error
|
||||
port int
|
||||
}
|
||||
|
||||
func newHTTPSSniffListener(ln net.Listener, port int) net.Listener {
|
||||
l := &httpsSniffListener{Listener: ln, conns: make(chan net.Conn, 16), errs: make(chan error, 1), port: port}
|
||||
go func() {
|
||||
for {
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
l.errs <- err
|
||||
return
|
||||
}
|
||||
go l.sniff(c)
|
||||
}
|
||||
}()
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *httpsSniffListener) Accept() (net.Conn, error) {
|
||||
select {
|
||||
case c := <-l.conns:
|
||||
return c, nil
|
||||
case err := <-l.errs:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (l *httpsSniffListener) sniff(c net.Conn) {
|
||||
_ = c.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||
br := bufio.NewReader(c)
|
||||
b, err := br.Peek(1)
|
||||
if err != nil {
|
||||
_ = c.Close()
|
||||
return
|
||||
}
|
||||
_ = c.SetReadDeadline(time.Time{})
|
||||
if b[0] == 0x16 {
|
||||
l.conns <- &peekedConn{Conn: c, r: br}
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
_ = c.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||
req, err := http.ReadRequest(br)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
host := req.Host
|
||||
if h, _, err := net.SplitHostPort(host); err == nil {
|
||||
host = h
|
||||
}
|
||||
if net.ParseIP(host) == nil && !validHostname(strings.ToLower(host)) {
|
||||
host = "localhost"
|
||||
}
|
||||
target := "https://" + net.JoinHostPort(host, strconv.Itoa(l.port)) + req.URL.RequestURI()
|
||||
_, _ = c.Write([]byte("HTTP/1.1 308 Permanent Redirect\r\nLocation: " + target + "\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"))
|
||||
}
|
||||
|
||||
// peekedConn — соединение, у которого уже прочитан (подсмотрен) первый байт.
|
||||
type peekedConn struct {
|
||||
net.Conn
|
||||
r *bufio.Reader
|
||||
}
|
||||
|
||||
func (p *peekedConn) Read(b []byte) (int, error) { return p.r.Read(b) }
|
||||
|
||||
// serverIPs — адреса этого сервера (для проверки, куда указывает доменное имя).
|
||||
func serverIPs() []string {
|
||||
var out []string
|
||||
addrs, _ := net.InterfaceAddrs()
|
||||
for _, a := range addrs {
|
||||
if n, ok := a.(*net.IPNet); ok && !n.IP.IsLoopback() && !n.IP.IsLinkLocalUnicast() {
|
||||
out = append(out, n.IP.String())
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package lkgateway
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// http:// на порт HTTPS — 308 на https:// того же имени и порта; первый байт TLS проходит дальше.
|
||||
func TestHTTPSSniffRedirect(t *testing.T) {
|
||||
raw, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ln := newHTTPSSniffListener(raw, 8443)
|
||||
defer ln.Close()
|
||||
go func() { // TLS-соединения отдаются Accept
|
||||
c, err := ln.Accept()
|
||||
if err == nil {
|
||||
b := make([]byte, 1)
|
||||
_, _ = c.Read(b)
|
||||
_, _ = c.Write(b)
|
||||
c.Close()
|
||||
}
|
||||
}()
|
||||
c, err := net.Dial("tcp", raw.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _ = c.Write([]byte("GET /admin/setup?x=1 HTTP/1.1\r\nHost: m2m.corp.fontvielle.ru:8443\r\n\r\n"))
|
||||
resp, err := http.ReadResponse(bufio.NewReader(c), nil)
|
||||
c.Close()
|
||||
if err != nil || resp.StatusCode != 308 || resp.Header.Get("Location") != "https://m2m.corp.fontvielle.ru:8443/admin/setup?x=1" {
|
||||
t.Fatalf("ответ: %v %v %q", err, resp, resp.Header.Get("Location"))
|
||||
}
|
||||
c2, _ := net.Dial("tcp", raw.Addr().String())
|
||||
_, _ = c2.Write([]byte{0x16})
|
||||
b := make([]byte, 1)
|
||||
if _, err := c2.Read(b); err != nil || b[0] != 0x16 || !strings.Contains(raw.Addr().String(), "127.0.0.1") {
|
||||
t.Fatalf("TLS-байт не прошёл: %v %x", err, b)
|
||||
}
|
||||
c2.Close()
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{{define "content"}}
|
||||
{{template "depoProcsBanner" .}}
|
||||
{{/* ===== Оператор-дашборд (Apple-стиль): приветствие → статус → плитки задач → сводка ===== */}}
|
||||
|
||||
<div class="hero">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{{define "content"}}
|
||||
{{template "depoProcsBanner" .}}
|
||||
<script>if(/^#(sec-)?(tests|units)$/.test(location.hash)) location.replace('/admin/status#diag');</script>
|
||||
{{if .Flash}}<div style="padding:12px 16px;background:var(--ok-weak);background:var(--ok-weak);border-radius:8px;margin-bottom:16px">{{.Flash}}</div>{{end}}
|
||||
|
||||
@@ -939,7 +940,7 @@
|
||||
<input id="set-hostnames" name="hostnames" value="{{range $i, $n := .Settings.HTTPS.Hostnames}}{{if $i}} {{end}}{{$n}}{{end}}" placeholder="например, m2m.corp.example.ru (несколько — через пробел)" style="min-width:320px;flex:1">
|
||||
<button type="submit" class="btn">Сохранить</button>
|
||||
</div>
|
||||
<p class="muted" style="font-size:12.5px;margin:4px 0 0">bj впишет имя в свой сертификат — интерфейс будет открываться по нему без предупреждения о несовпадении имени. DNS-запись заводит администратор сети.</p>
|
||||
<p class="muted" style="font-size:12.5px;margin:4px 0 0">Сначала администратор сети заводит в DNS запись A на этот сервер ({{range $i, $ip := serverIPs}}{{if $i}}, {{end}}{{$ip}}{{end}}); bj проверит, что имя находится и указывает сюда, и впишет его в сертификат. Адрес bj — <code>https://имя:{{httpsPort}}/</code>.</p>
|
||||
</form>
|
||||
<details style="margin-top:8px"><summary style="cursor:pointer;color:var(--accent)">Сертификат организации</summary>
|
||||
<form method="post" action="/admin/setup/https/cert" enctype="multipart/form-data" style="display:grid;gap:8px;max-width:520px;margin-top:8px">
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
{{define "depoProcsBanner"}}{{if and (can "settings") depoProcsPending}}<div class="card" id="depo-procs-pending" style="border-color:var(--warn);background:var(--warn-weak)">
|
||||
<h2 style="margin:0 0 6px">Обновление bj требует применить скрипты в базе DEPO</h2>
|
||||
<p style="margin:0 0 8px">В этой версии bj обновлены процедуры обработки M2M в DEPO ({{depoProcOrder}}). Для их установки нужны <b>повышенные права на базу данных</b>: введите логин и пароль администратора SQL (sysadmin, обычно sa). Они используются один раз и не сохраняются.</p>
|
||||
<form method="post" action="/admin/setup/fancy/depo-procs/install" style="display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end" onsubmit="return confirm('Применить обновлённые процедуры в базе DEPO? CREATE OR ALTER — данные не меняются.')">
|
||||
<input type="hidden" name="section" value="sec-fancy-depo-procs">
|
||||
<label style="display:flex;flex-direction:column;gap:3px">Логин администратора SQL<input type="text" name="admin_user" autocomplete="off" placeholder="sa" required></label>
|
||||
<label style="display:flex;flex-direction:column;gap:3px">Пароль<input type="password" name="admin_password" autocomplete="new-password" required></label>
|
||||
<button type="submit" class="btn">Применить скрипты</button>
|
||||
<a href="/admin/setup/fancy/depo-procs/script.sql" class="muted" style="font-size:12.5px">или скачать скрипт для администратора БД</a>
|
||||
</form>
|
||||
</div>{{end}}{{end}}
|
||||
{{define "layout"}}<!DOCTYPE html>
|
||||
<html lang="ru" data-theme="light">
|
||||
<head>
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
|
||||
<div class="card" id="sec-fancy-depo-procs">
|
||||
<h2><span class="dot {{if .Settings.Fancy.DepoProcCheckOK}}ok{{else}}warn{{end}}"></span>Процедуры обработки bj2depo</h2>
|
||||
<p class="muted">Процедуры <code>FTV_M2M_PROCESS_*</code> и <code>FTV_M2M_JOB</code> встроены в bj. Сначала «Проверить возможности» (ничего не меняет), затем «Установить» — bj выполнит их в базе DEPO в порядке WIRING→OPERATION→EVENTS→CREATE→JOB и выдаст логину bj право запуска.</p>
|
||||
<p class="muted">Процедуры <code>FTV_M2M_*</code> встроены в bj. Сначала «Проверить возможности» (ничего не меняет), затем «Установить» — bj выполнит их в базе DEPO в порядке {{depoProcOrder}} и выдаст логину bj право запуска.</p>
|
||||
<form method="post" style="display:grid;gap:10px;max-width:720px">
|
||||
<input type="hidden" name="section" value="sec-fancy-depo-procs">
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px">
|
||||
|
||||
Reference in New Issue
Block a user