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:
fontvielle
2026-10-22 13:33:42 +03:00
parent 77b0ab1117
commit 76e6f82369
6 changed files with 177 additions and 22 deletions
+23 -15
View File
@@ -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,
+1 -2
View File
@@ -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:// сам). Сертификат самоподписанный — браузер один раз попросит подтвердить исключение; "+
"без предупреждения — после загрузки сертификата организации («Сертификат организации» ниже).")
})
}
+132 -3
View File
@@ -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">