HTTPS: одна кнопка «Сохранить» на раздел (перенаправление, доменное имя, адрес без порта)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()) + "/."
|
||||
}
|
||||
|
||||
@@ -929,24 +929,15 @@
|
||||
<h2><span class="dot {{if and .Settings.HTTPS.RedirectOn (not $h.Err)}}ok{{else}}warn{{end}}"></span>HTTPS — шифрование входа в bj</h2>
|
||||
<p class="muted">Пароли администратора, баз данных, хранилища и ключ продукта не должны идти по сети открытым текстом. bj всегда слушает HTTPS: <a href="https://{{serverIP}}:{{if .Settings.HTTPS.Port}}{{.Settings.HTTPS.Port}}{{else}}8443{{end}}/admin/">https://{{serverIP}}:{{if .Settings.HTTPS.Port}}{{.Settings.HTTPS.Port}}{{else}}8443{{end}}/</a>. Модули bj и API на этом сервере работают по HTTP, как раньше.</p>
|
||||
<p style="margin:8px 0">{{if $h.Err}}<span style="color:var(--err)">✗ сертификат: {{$h.Err}}</span>{{else}}Сертификат: <b>{{$h.CN}}</b>, до {{$h.Until.Format "02.01.2006"}} — {{if .Settings.HTTPS.Custom}}сертификат организации{{else}}самоподписанный: браузер один раз попросит подтвердить исключение; чтобы не просил — загрузите сертификат организации{{end}}.{{end}}</p>
|
||||
<form method="post" action="/admin/setup/https" style="margin:8px 0">
|
||||
<form method="post" action="/admin/setup/https" style="margin:8px 0" onsubmit="var p=this.std_ports;return p.checked==p.defaultChecked||confirm('Сохранить? bj перезапустится на несколько секунд.')">
|
||||
<input type="hidden" name="section" value="sec-https">
|
||||
<label style="display:flex;align-items:center;gap:8px;font-weight:400"><input type="checkbox" name="redirect" style="width:auto"{{if .Settings.HTTPS.RedirectOn}} checked{{end}}> <b>Открывать bj только по HTTPS</b> — браузер с http:// перенаправляется на https://</label>
|
||||
<button type="submit" class="btn" style="margin-top:8px">Сохранить</button>
|
||||
</form>
|
||||
<form method="post" action="/admin/setup/hostname" style="margin:12px 0 8px">
|
||||
<input type="hidden" name="section" value="sec-https">
|
||||
<label for="set-hostnames" style="display:block;margin-bottom:4px">Доменное имя сервера bj</label>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||||
<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>
|
||||
<label for="set-hostnames" style="display:block;margin:12px 0 4px">Доменное имя сервера bj</label>
|
||||
<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;width:100%">
|
||||
<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://имя{{httpsPortSuffix}}/</code>.</p>
|
||||
</form>
|
||||
<form method="post" action="/admin/setup/https/stdports" style="margin:4px 0 8px">
|
||||
<input type="hidden" name="section" value="sec-https">
|
||||
<label style="display:flex;align-items:center;gap:8px;font-weight:400"><input type="checkbox" name="std_ports" style="width:auto"{{if .Settings.HTTPS.StdPorts}} checked{{end}}> <b>Адрес без номера порта</b> — https://имя/ вместо https://имя:{{httpsOwnPort}}/ (bj слушает 443, а 80 переводит на https://)</label>
|
||||
<label style="display:flex;align-items:center;gap:8px;font-weight:400;margin-top:10px"><input type="checkbox" name="std_ports" style="width:auto"{{if .Settings.HTTPS.StdPorts}} checked{{end}}> <b>Адрес без номера порта</b> — https://имя/ вместо https://имя:{{httpsOwnPort}}/ (bj слушает 443, а 80 переводит на https://)</label>
|
||||
{{with stdPortsState}}<p style="margin:4px 0 0;font-size:12.5px">{{if eq . "ok"}}<span style="color:var(--ok)">✓ работает: порты 443 и 80</span>{{else}}<span style="color:var(--warn)">⚠ {{.}} — нажмите «Сохранить» ещё раз</span>{{end}}</p>{{end}}
|
||||
<button type="submit" class="btn" style="margin-top:8px" onclick="return confirm('Сохранить? bj перезапустится на несколько секунд.')">Сохранить</button>
|
||||
<button type="submit" class="btn" style="margin-top:12px">Сохранить</button>
|
||||
</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">
|
||||
|
||||
Reference in New Issue
Block a user