HTTPS: адрес без номера порта — 443 и 80
«Сервер: HTTPS» → «Адрес без номера порта»: bj слушает 443 (тот же HTTPS) и 80 (переход на https://имя/), переходы и адреса — без «:8443». Право на порты <1024 — drop-in systemd AmbientCapabilities=CAP_NET_BIND_SERVICE через root-помощника (bj-system-admin ports on|off, bj перезапускается). Нет права/порт занят — bj работает по 8443, в настройках видна причина. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
This commit is contained in:
+23
-15
@@ -10,6 +10,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -140,21 +141,28 @@ 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,
|
||||
"serverIPs": serverIPs,
|
||||
"httpsPort": func() int { return globalRC.Snapshot().HTTPS.port() },
|
||||
"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,
|
||||
"httpsPortSuffix": func() string {
|
||||
if p := globalRC.Snapshot().HTTPS.publicPort(); p != 443 {
|
||||
return ":" + strconv.Itoa(p)
|
||||
}
|
||||
return ""
|
||||
},
|
||||
"httpsOwnPort": func() int { return globalRC.Snapshot().HTTPS.port() },
|
||||
"stdPortsState": stdPortsState,
|
||||
"depoProcsPending": depoProcsPending,
|
||||
"depoProcOrder": func() string { return depoProcOrderText("→") },
|
||||
"nsdSegPop": nsdSegPop,
|
||||
|
||||
@@ -442,8 +442,7 @@ func registerHostname(mux *http.ServeMux, rc *RuntimeConfig) {
|
||||
setupFlash(w, r, "Доменные имена убраны. Сертификат перевыпущен на имя хоста и IP.")
|
||||
return
|
||||
}
|
||||
setupFlash(w, r, "Готово: "+strings.Join(resolved, "; ")+" — имя есть в DNS и вписано в сертификат. Адрес bj: https://"+names[0]+":"+
|
||||
strconv.Itoa(h.port())+"/ (http:// перебросит на https:// сам). Сертификат самоподписанный — браузер один раз попросит подтвердить исключение; "+
|
||||
setupFlash(w, r, "Готово: "+strings.Join(resolved, "; ")+" — имя есть в DNS и вписано в сертификат. Адрес bj: https://"+httpsHostPort(names[0], h.publicPort())+"/ (http:// перебросит на https:// сам). Сертификат самоподписанный — браузер один раз попросит подтвердить исключение; "+
|
||||
"без предупреждения — после загрузки сертификата организации («Сертификат организации» ниже).")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ package lkgateway
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
@@ -41,6 +42,33 @@ type HTTPSSettings struct {
|
||||
// (контроллер домена) направляет их на bj, а bj вписывает их в самоподписанный сертификат,
|
||||
// чтобы браузер не ругался на имя. Пусто — только имя хоста и его IP.
|
||||
Hostnames []string `json:"hostnames,omitempty"`
|
||||
// StdPorts — bj слушает и стандартные порты: 443 (HTTPS) и 80 (переход на https://), адрес без «:8443».
|
||||
StdPorts bool `json:"std_ports,omitempty"`
|
||||
}
|
||||
|
||||
// https443Up — bj слушает 443; std443Err — почему не слушает (нет права — включить ещё раз).
|
||||
var (
|
||||
https443Up atomic.Bool
|
||||
std443Err atomic.Value // string
|
||||
)
|
||||
|
||||
// publicPort — порт HTTPS в адресах и переходах: 443, если bj его слушает, иначе свой (8443).
|
||||
func (h HTTPSSettings) publicPort() int {
|
||||
if h.StdPorts && https443Up.Load() {
|
||||
return 443
|
||||
}
|
||||
return h.port()
|
||||
}
|
||||
|
||||
// httpsHostPort — «имя» для 443 и «имя:порт» для остальных.
|
||||
func httpsHostPort(host string, port int) string {
|
||||
if port == 443 {
|
||||
if net.ParseIP(host) != nil && strings.Contains(host, ":") {
|
||||
return "[" + host + "]"
|
||||
}
|
||||
return host
|
||||
}
|
||||
return net.JoinHostPort(host, strconv.Itoa(port))
|
||||
}
|
||||
|
||||
// RedirectOn — браузер с HTTP перенаправляется на HTTPS.
|
||||
@@ -173,7 +201,7 @@ func httpsRedirect(rc *RuntimeConfig, next http.Handler) http.Handler {
|
||||
if hh, _, err := net.SplitHostPort(hn); err == nil {
|
||||
hn = hh
|
||||
}
|
||||
http.Redirect(w, r, "https://"+net.JoinHostPort(hn, strconv.Itoa(h.port()))+r.URL.RequestURI(), http.StatusFound)
|
||||
http.Redirect(w, r, "https://"+httpsHostPort(hn, h.publicPort())+r.URL.RequestURI(), http.StatusFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -254,6 +282,9 @@ func startHTTPS(rc *RuntimeConfig, handler http.Handler) *http.Server {
|
||||
log.Printf("https: %v", err)
|
||||
}
|
||||
}()
|
||||
if rc.Snapshot().HTTPS.StdPorts {
|
||||
startStdPorts(srv)
|
||||
}
|
||||
return srv
|
||||
}
|
||||
|
||||
@@ -271,10 +302,49 @@ func registerHTTPS(mux *http.ServeMux, rc *RuntimeConfig) {
|
||||
}
|
||||
msg := "HTTPS: перенаправление с HTTP выключено"
|
||||
if h.RedirectOn() {
|
||||
msg = "HTTPS: браузеры теперь перенаправляются на https://…:" + strconv.Itoa(h.port())
|
||||
msg = "HTTPS: браузеры теперь перенаправляются на https://" + httpsHostPort("…", h.publicPort())
|
||||
}
|
||||
setupFlash(w, r, msg)
|
||||
})
|
||||
// адрес без номера порта: 443 и 80 (право на порты — через root-помощника, bj перезапускается)
|
||||
mux.HandleFunc("/admin/setup/https/stdports", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
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())+"/.")
|
||||
}
|
||||
})
|
||||
// свой сертификат организации (PEM: сертификат с цепочкой и закрытый ключ)
|
||||
mux.HandleFunc("/admin/setup/https/cert", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
@@ -389,7 +459,11 @@ func (l *httpsSniffListener) sniff(c net.Conn) {
|
||||
if net.ParseIP(host) == nil && !validHostname(strings.ToLower(host)) {
|
||||
host = "localhost"
|
||||
}
|
||||
target := "https://" + net.JoinHostPort(host, strconv.Itoa(l.port)) + req.URL.RequestURI()
|
||||
port := l.port
|
||||
if globalRC != nil && globalRC.Snapshot().HTTPS.publicPort() == 443 {
|
||||
port = 443
|
||||
}
|
||||
target := "https://" + httpsHostPort(host, 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"))
|
||||
}
|
||||
|
||||
@@ -412,3 +486,58 @@ func serverIPs() []string {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// startStdPorts — 443 (тот же HTTPS) и 80 (переход на https://имя/). Нет права на порты <1024 — bj
|
||||
// работает по 8443, а в настройках видно, что нужно включить ещё раз (выдать право через помощника).
|
||||
func startStdPorts(srv *http.Server) {
|
||||
ln, err := net.Listen("tcp", ":443")
|
||||
if err != nil {
|
||||
std443Err.Store(err.Error())
|
||||
log.Printf("https: порт 443: %v — адрес без номера порта недоступен", err)
|
||||
return
|
||||
}
|
||||
std443Err.Store("")
|
||||
https443Up.Store(true)
|
||||
go func() {
|
||||
log.Printf("lk-gateway: listen https :443")
|
||||
if err := srv.ServeTLS(newHTTPSSniffListener(ln, 443), "", ""); err != nil && err != http.ErrServerClosed {
|
||||
https443Up.Store(false)
|
||||
log.Printf("https :443: %v", err)
|
||||
}
|
||||
}()
|
||||
ln80, err := net.Listen("tcp", ":80")
|
||||
if err != nil {
|
||||
log.Printf("http: порт 80: %v — http://имя/ без порта не откроется (https://имя/ работает)", err)
|
||||
return
|
||||
}
|
||||
redir := &http.Server{ReadHeaderTimeout: 5 * time.Second, Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
host := r.Host
|
||||
if h, _, err := net.SplitHostPort(host); err == nil {
|
||||
host = h
|
||||
}
|
||||
if net.ParseIP(host) == nil && !validHostname(strings.ToLower(host)) {
|
||||
http.Error(w, "bad host", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "https://"+httpsHostPort(host, 443)+r.URL.RequestURI(), http.StatusMovedPermanently)
|
||||
})}
|
||||
go func() {
|
||||
log.Printf("lk-gateway: listen http :80 (переход на https://)")
|
||||
_ = redir.Serve(ln80)
|
||||
}()
|
||||
}
|
||||
|
||||
// stdPortsState — для настроек: включено ли и работает ли.
|
||||
func stdPortsState() string {
|
||||
h := globalRC.Snapshot().HTTPS
|
||||
switch {
|
||||
case !h.StdPorts:
|
||||
return ""
|
||||
case https443Up.Load():
|
||||
return "ok"
|
||||
}
|
||||
if e, _ := std443Err.Load().(string); e != "" {
|
||||
return e
|
||||
}
|
||||
return "ожидает перезапуска bj"
|
||||
}
|
||||
|
||||
@@ -940,7 +940,13 @@
|
||||
<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">Сначала администратор сети заводит в DNS запись A на этот сервер ({{range $i, $ip := serverIPs}}{{if $i}}, {{end}}{{$ip}}{{end}}); bj проверит, что имя находится и указывает сюда, и впишет его в сертификат. Адрес bj — <code>https://имя:{{httpsPort}}/</code>.</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://имя{{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>
|
||||
{{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>
|
||||
</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