diff --git a/internal/lkgateway/timesync.go b/internal/lkgateway/timesync.go index 7cf93b6..d0c3c3d 100644 --- a/internal/lkgateway/timesync.go +++ b/internal/lkgateway/timesync.go @@ -376,73 +376,73 @@ func registerHostname(mux *http.ServeMux, rc *RuntimeConfig) { http.Error(w, "method", http.StatusMethodNotAllowed) return } - raw := strings.NewReplacer(",", " ", ";", " ", "\n", " ", "\r", " ").Replace(r.FormValue("hostnames")) - var names []string - for _, f := range strings.Fields(raw) { - f = strings.ToLower(strings.TrimSpace(f)) - f = strings.TrimPrefix(f, "https://") - f = strings.TrimPrefix(f, "http://") - if i := strings.IndexByte(f, '/'); i >= 0 { - f = f[:i] - } - if h, _, err := net.SplitHostPort(f); err == nil { - f = h - } - if f == "" { - continue - } - if !validHostname(f) { - setupFlash(w, r, "Доменное имя «"+f+"» задано неверно. Пример: m2m.corp.example.ru") - return - } - 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 { - setupFlash(w, r, "Имена сервера: "+err.Error()) - return - } - if h.Custom { - setupFlash(w, r, "Имена сохранены. Сейчас используется загруженный сертификат организации — проверьте, что эти имена есть в нём.") - return - } - if err := ensureSelfSigned(true); err != nil { - setupFlash(w, r, "Имена сохранены, но сертификат не перевыпущен: "+err.Error()) - return - } - if len(names) == 0 { - setupFlash(w, r, "Доменные имена убраны. Сертификат перевыпущен на имя хоста и IP.") - return - } - setupFlash(w, r, "Готово: "+strings.Join(resolved, "; ")+" — имя есть в DNS и вписано в сертификат. Адрес bj: https://"+httpsHostPort(names[0], h.publicPort())+"/ (http:// перебросит на https:// сам). Сертификат самоподписанный — браузер один раз попросит подтвердить исключение; "+ - "без предупреждения — после загрузки сертификата организации («Сертификат организации» ниже).") + msg, _ := saveHostnames(r.Context(), rc, r.FormValue("hostnames")) + setupFlash(w, r, msg) }) } + +// saveHostnames — доменные имена сервера: проверка DNS (имя указывает на этот сервер), сохранение, +// перевыпуск самоподписанного сертификата. Возвращает текст для пользователя и успех. +func saveHostnames(ctx0 context.Context, rc *RuntimeConfig, raw string) (string, bool) { + raw = strings.NewReplacer(",", " ", ";", " ", "\n", " ", "\r", " ").Replace(raw) + var names []string + for _, f := range strings.Fields(raw) { + f = strings.ToLower(strings.TrimSpace(f)) + f = strings.TrimPrefix(f, "https://") + f = strings.TrimPrefix(f, "http://") + if i := strings.IndexByte(f, '/'); i >= 0 { + f = f[:i] + } + if h, _, err := net.SplitHostPort(f); err == nil { + f = h + } + if f == "" { + continue + } + if !validHostname(f) { + return "Доменное имя «" + f + "» задано неверно. Пример: m2m.corp.example.ru", false + } + 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(ctx0, 4*time.Second) + addrs, err := net.DefaultResolver.LookupHost(ctx, n) + cancel() + if err != nil || len(addrs) == 0 { + return "Имя «" + n + "» не найдено в DNS. Попросите администратора сети добавить на DNS-сервере (контроллере домена) запись A: " + + n + " → " + strings.Join(serverIPs(), " или ") + " — и нажмите «Сохранить» ещё раз.", false + } + ok := false + for _, a := range addrs { + ok = ok || mine[a] + } + if !ok && len(mine) > 0 { + return "Имя «" + n + "» в DNS указывает на " + strings.Join(addrs, ", ") + ", а у сервера bj адрес " + strings.Join(serverIPs(), ", ") + + ". Исправьте запись A на DNS-сервере и нажмите «Сохранить» ещё раз.", false + } + resolved = append(resolved, n+" → "+strings.Join(addrs, ", ")) + } + h := rc.Snapshot().HTTPS + h.Hostnames = names + if err := rc.UpdateHTTPS(h); err != nil { + return "Имена сервера: " + err.Error(), false + } + if h.Custom { + return "Имена сохранены. Сейчас используется загруженный сертификат организации — проверьте, что эти имена есть в нём.", true + } + if err := ensureSelfSigned(true); err != nil { + return "Имена сохранены, но сертификат не перевыпущен: " + err.Error(), false + } + if len(names) == 0 { + return "Доменные имена убраны. Сертификат перевыпущен на имя хоста и IP.", true + } + return "Готово: " + strings.Join(resolved, "; ") + " — имя есть в DNS и вписано в сертификат. Адрес bj: https://" + httpsHostPort(names[0], h.publicPort()) + "/ (http:// перебросит на https:// сам). Сертификат самоподписанный — браузер один раз попросит подтвердить исключение; " + + "без предупреждения — после загрузки сертификата организации («Сертификат организации» ниже).", true +} diff --git a/internal/lkgateway/tlsserve.go b/internal/lkgateway/tlsserve.go index 04c5815..ca9fd11 100644 --- a/internal/lkgateway/tlsserve.go +++ b/internal/lkgateway/tlsserve.go @@ -294,17 +294,38 @@ func registerHTTPS(mux *http.ServeMux, rc *RuntimeConfig) { http.Error(w, "method", http.StatusMethodNotAllowed) return } + // одна форма раздела: перенаправление, доменное имя, адрес без порта. Применяется только изменённое; + // порты — последними: bj после них перезапускается. h := rc.Snapshot().HTTPS - h.RedirectOff = r.FormValue("redirect") == "" - if err := rc.UpdateHTTPS(h); err != nil { - setupFlash(w, r, "HTTPS: "+err.Error()) - return + var msgs []string + if off := r.FormValue("redirect") == ""; off != h.RedirectOff { + h.RedirectOff = off + if err := rc.UpdateHTTPS(h); err != nil { + setupFlash(w, r, "HTTPS: "+err.Error()) + return + } + if h.RedirectOn() { + msgs = append(msgs, "Браузеры перенаправляются с http:// на https://.") + } else { + msgs = append(msgs, "Перенаправление с http:// выключено.") + } } - msg := "HTTPS: перенаправление с HTTP выключено" - if h.RedirectOn() { - msg = "HTTPS: браузеры теперь перенаправляются на https://" + httpsHostPort("…", h.publicPort()) + if _, sent := r.Form["hostnames"]; sent && strings.Join(strings.Fields(strings.ToLower(r.FormValue("hostnames"))), " ") != strings.Join(h.Hostnames, " ") { + msg, ok := saveHostnames(r.Context(), rc, r.FormValue("hostnames")) + msgs = append(msgs, msg) + if !ok { + setupFlash(w, r, strings.Join(msgs, " ")) + return + } } - setupFlash(w, r, msg) + // включено, но порт 443 не поднялся — «Сохранить» повторяет выдачу права + if on := r.FormValue("std_ports") != ""; on != rc.Snapshot().HTTPS.StdPorts || (on && stdPortsState() != "ok") { + msgs = append(msgs, applyStdPorts(r.Context(), rc, on)) + } + if len(msgs) == 0 { + msgs = append(msgs, "HTTPS: изменений нет.") + } + setupFlash(w, r, strings.Join(msgs, " ")) }) // адрес без номера порта: 443 и 80 (право на порты — через root-помощника, bj перезапускается) mux.HandleFunc("/admin/setup/https/stdports", func(w http.ResponseWriter, r *http.Request) { @@ -312,38 +333,7 @@ func registerHTTPS(mux *http.ServeMux, rc *RuntimeConfig) { http.Error(w, "method", http.StatusMethodNotAllowed) return } - on := r.FormValue("std_ports") != "" - h := rc.Snapshot().HTTPS - h.StdPorts = on - if err := rc.UpdateHTTPS(h); err != nil { - setupFlash(w, r, "HTTPS: "+err.Error()) - return - } - mode := "off" - if on { - mode = "on" - } - ctx, cancel := context.WithTimeout(r.Context(), 40*time.Second) - res, err := runKeymediaOp(ctx, "system-ports", map[string]string{"extra": mode}) - cancel() - if err == nil { - if ok, _ := res["ok"].(bool); !ok { - err = fmt.Errorf("%v", res["error"]) - } - } - if err != nil { - setupFlash(w, r, "Адрес без номера порта: настройка сохранена, но право на порты не выдано — "+err.Error()) - return - } - name := "имя-сервера" - if len(h.Hostnames) > 0 { - name = h.Hostnames[0] - } - if on { - setupFlash(w, r, "Готово: bj перезапускается (несколько секунд) и будет открываться по https://"+name+"/ — без :"+strconv.Itoa(h.port())+"; http://"+name+"/ перейдёт на https:// сам. Обновите страницу через 10 секунд.") - } else { - setupFlash(w, r, "Адрес без номера порта выключен: bj перезапускается и открывается по https://"+name+":"+strconv.Itoa(h.port())+"/.") - } + setupFlash(w, r, applyStdPorts(r.Context(), rc, r.FormValue("std_ports") != "")) }) // свой сертификат организации (PEM: сертификат с цепочкой и закрытый ключ) mux.HandleFunc("/admin/setup/https/cert", func(w http.ResponseWriter, r *http.Request) { @@ -541,3 +531,36 @@ func stdPortsState() string { } return "ожидает перезапуска bj" } + +// applyStdPorts — адрес без номера порта: сохранить настройку и выдать/снять службе право на 443/80 +// (через root-помощника; bj после этого перезапускается). Возвращает текст для пользователя. +func applyStdPorts(ctx0 context.Context, rc *RuntimeConfig, on bool) string { + h := rc.Snapshot().HTTPS + h.StdPorts = on + if err := rc.UpdateHTTPS(h); err != nil { + return "HTTPS: " + err.Error() + } + mode := "off" + if on { + mode = "on" + } + ctx, cancel := context.WithTimeout(ctx0, 40*time.Second) + res, err := runKeymediaOp(ctx, "system-ports", map[string]string{"extra": mode}) + cancel() + if err == nil { + if ok, _ := res["ok"].(bool); !ok { + err = fmt.Errorf("%v", res["error"]) + } + } + if err != nil { + return "Адрес без номера порта: настройка сохранена, но право на порты не выдано — " + err.Error() + } + name := "имя-сервера" + if len(h.Hostnames) > 0 { + name = h.Hostnames[0] + } + if on { + return "Готово: bj перезапускается (несколько секунд) и будет открываться по https://" + name + "/ — без :" + strconv.Itoa(h.port()) + "; http://" + name + "/ перейдёт на https:// сам. Обновите страницу через 10 секунд." + } + return "Адрес без номера порта выключен: bj перезапускается и открывается по https://" + name + ":" + strconv.Itoa(h.port()) + "/." +} diff --git a/internal/lkgateway/web/templates/admin_setup.html b/internal/lkgateway/web/templates/admin_setup.html index 6198335..de9383f 100644 --- a/internal/lkgateway/web/templates/admin_setup.html +++ b/internal/lkgateway/web/templates/admin_setup.html @@ -929,24 +929,15 @@

HTTPS — шифрование входа в bj

Пароли администратора, баз данных, хранилища и ключ продукта не должны идти по сети открытым текстом. bj всегда слушает HTTPS: https://{{serverIP}}:{{if .Settings.HTTPS.Port}}{{.Settings.HTTPS.Port}}{{else}}8443{{end}}/. Модули bj и API на этом сервере работают по HTTP, как раньше.

{{if $h.Err}}✗ сертификат: {{$h.Err}}{{else}}Сертификат: {{$h.CN}}, до {{$h.Until.Format "02.01.2006"}} — {{if .Settings.HTTPS.Custom}}сертификат организации{{else}}самоподписанный: браузер один раз попросит подтвердить исключение; чтобы не просил — загрузите сертификат организации{{end}}.{{end}}

-
+ + - -
-
- - -
- - -
+ +

Сначала администратор сети заводит в DNS запись A на этот сервер ({{range $i, $ip := serverIPs}}{{if $i}}, {{end}}{{$ip}}{{end}}); bj проверит, что имя находится и указывает сюда, и впишет его в сертификат. Адрес bj — https://имя{{httpsPortSuffix}}/.

-
-
- - + {{with stdPortsState}}

{{if eq . "ok"}}✓ работает: порты 443 и 80{{else}}⚠ {{.}} — нажмите «Сохранить» ещё раз{{end}}

{{end}} - +
Сертификат организации