7fd4bd0af9
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
567 lines
20 KiB
Go
567 lines
20 KiB
Go
package lkgateway
|
||
|
||
// HTTPS для веб-интерфейса bj (05.10.2026). Пароли администратора, SQL, хранилища и ключ продукта не должны
|
||
// идти по сети открытым текстом. bj слушает HTTPS рядом с HTTP (по умолчанию :8443) с самоподписанным
|
||
// сертификатом, который создаёт сам при первом запуске (на имя сервера и все его адреса); можно загрузить
|
||
// сертификат организации. Перенаправление браузера с HTTP на HTTPS — настройкой: локальные модули bj и API
|
||
// ходят по HTTP на localhost и не перенаправляются.
|
||
|
||
import (
|
||
"bufio"
|
||
"context"
|
||
"crypto/ecdsa"
|
||
"crypto/elliptic"
|
||
"crypto/rand"
|
||
"crypto/tls"
|
||
"crypto/x509"
|
||
"crypto/x509/pkix"
|
||
"encoding/pem"
|
||
"fmt"
|
||
"io"
|
||
"log"
|
||
"math/big"
|
||
"net"
|
||
"net/http"
|
||
"os"
|
||
"path/filepath"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"sync/atomic"
|
||
"time"
|
||
)
|
||
|
||
// HTTPSSettings — HTTPS веб-интерфейса.
|
||
type HTTPSSettings struct {
|
||
Port int `json:"port,omitempty"` // 0 — 8443
|
||
// RedirectOff — не перенаправлять браузер с HTTP на HTTPS. С 06.10.2026 перенаправление включено сразу
|
||
// (самоподписанный сертификат bj создаёт сам): пароли не должны идти по сети открытым текстом.
|
||
RedirectOff bool `json:"redirect_off,omitempty"`
|
||
Custom bool `json:"custom,omitempty"` // загружен сертификат организации (не самоподписанный)
|
||
// Hostnames — доменные имена сервера (например, m2m.corp.example.ru). DNS организации
|
||
// (контроллер домена) направляет их на 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.
|
||
func (h HTTPSSettings) RedirectOn() bool { return !h.RedirectOff }
|
||
|
||
// httpsUp — HTTPS действительно слушает порт: иначе перенаправление оставило бы без входа в bj.
|
||
var httpsUp atomic.Bool
|
||
|
||
func (h HTTPSSettings) port() int {
|
||
if h.Port > 0 && h.Port < 65536 {
|
||
return h.Port
|
||
}
|
||
return 8443
|
||
}
|
||
|
||
var tlsDir = "/var/lib/bj/.bj/tls"
|
||
|
||
func tlsCertPath() string { return filepath.Join(tlsDir, "cert.pem") }
|
||
func tlsKeyPath() string { return filepath.Join(tlsDir, "key.pem") }
|
||
|
||
// localAddrs — имя сервера и его адреса (для сертификата и подсказок).
|
||
func localAddrs() (string, []net.IP) {
|
||
host, _ := os.Hostname()
|
||
var ips []net.IP
|
||
if addrs, err := net.InterfaceAddrs(); err == nil {
|
||
for _, a := range addrs {
|
||
if n, ok := a.(*net.IPNet); ok && !n.IP.IsLinkLocalUnicast() {
|
||
ips = append(ips, n.IP)
|
||
}
|
||
}
|
||
}
|
||
return host, ips
|
||
}
|
||
|
||
// ensureSelfSigned — создать самоподписанный сертификат, если сертификата нет.
|
||
func ensureSelfSigned(force bool) error {
|
||
if !force {
|
||
if _, err := os.Stat(tlsCertPath()); err == nil {
|
||
return nil
|
||
}
|
||
}
|
||
if err := os.MkdirAll(tlsDir, 0o700); err != nil {
|
||
return err
|
||
}
|
||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
host, ips := localAddrs()
|
||
names := uniqStrings(append([]string{pickStr(host, "bj-server"), "localhost", pickStr(host, "bj-server") + ".local"}, customHostnames()...))
|
||
serial, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 62))
|
||
tpl := &x509.Certificate{
|
||
SerialNumber: serial,
|
||
Subject: pkix.Name{CommonName: pickStr(host, "bj-server"), Organization: []string{"Bridge&Join"}},
|
||
NotBefore: time.Now().Add(-time.Hour),
|
||
NotAfter: time.Now().AddDate(5, 0, 0),
|
||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||
DNSNames: names,
|
||
IPAddresses: ips,
|
||
}
|
||
der, err := x509.CreateCertificate(rand.Reader, tpl, tpl, &key.PublicKey, key)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
kb, err := x509.MarshalECPrivateKey(key)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if err := os.WriteFile(tlsKeyPath(), pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: kb}), 0o600); err != nil {
|
||
return err
|
||
}
|
||
return os.WriteFile(tlsCertPath(), pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), 0o644)
|
||
}
|
||
|
||
// tlsCertCache — сертификат с диска; перечитывается при замене файла (загрузка своего — без перезапуска).
|
||
var tlsCertCache struct {
|
||
sync.Mutex
|
||
mod time.Time
|
||
cert *tls.Certificate
|
||
}
|
||
|
||
func loadTLSCert(*tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||
tlsCertCache.Lock()
|
||
defer tlsCertCache.Unlock()
|
||
st, err := os.Stat(tlsCertPath())
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if tlsCertCache.cert != nil && st.ModTime().Equal(tlsCertCache.mod) {
|
||
return tlsCertCache.cert, nil
|
||
}
|
||
c, err := tls.LoadX509KeyPair(tlsCertPath(), tlsKeyPath())
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
tlsCertCache.cert, tlsCertCache.mod = &c, st.ModTime()
|
||
return &c, nil
|
||
}
|
||
|
||
// tlsCertInfo — кому выдан сертификат HTTPS и до какого числа.
|
||
func tlsCertInfo() (subject string, notAfter time.Time, err error) {
|
||
raw, err := os.ReadFile(tlsCertPath())
|
||
if err != nil {
|
||
return "", time.Time{}, err
|
||
}
|
||
b, _ := pem.Decode(raw)
|
||
if b == nil {
|
||
return "", time.Time{}, fmt.Errorf("сертификат не в формате PEM")
|
||
}
|
||
c, err := x509.ParseCertificate(b.Bytes)
|
||
if err != nil {
|
||
return "", time.Time{}, err
|
||
}
|
||
return c.Subject.CommonName, c.NotAfter, nil
|
||
}
|
||
|
||
// httpsRedirect — браузер с HTTP на HTTPS (если включено). API, проверка работы и запросы с этого же
|
||
// сервера (модули bj) не перенаправляются.
|
||
func httpsRedirect(rc *RuntimeConfig, next http.Handler) http.Handler {
|
||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
h := rc.Snapshot().HTTPS
|
||
if h.RedirectOn() && httpsUp.Load() && r.TLS == nil && !strings.HasPrefix(r.URL.Path, "/api/") && r.URL.Path != "/healthz" {
|
||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||
if err != nil {
|
||
host = r.RemoteAddr
|
||
}
|
||
if ip := net.ParseIP(host); ip == nil || !ip.IsLoopback() {
|
||
hn := r.Host
|
||
if hh, _, err := net.SplitHostPort(hn); err == nil {
|
||
hn = hh
|
||
}
|
||
http.Redirect(w, r, "https://"+httpsHostPort(hn, h.publicPort())+r.URL.RequestURI(), http.StatusFound)
|
||
return
|
||
}
|
||
}
|
||
next.ServeHTTP(w, r)
|
||
})
|
||
}
|
||
|
||
// selfSignedStale — самоподписанный сертификат выписан не на все нынешние адреса сервера (адрес сменили,
|
||
// копию восстановили на другой машине): браузер ругался бы ещё и на имя — выпускаем заново.
|
||
func selfSignedStale() bool {
|
||
raw, err := os.ReadFile(tlsCertPath())
|
||
if err != nil {
|
||
return false
|
||
}
|
||
b, _ := pem.Decode(raw)
|
||
if b == nil {
|
||
return false
|
||
}
|
||
c, err := x509.ParseCertificate(b.Bytes)
|
||
if err != nil {
|
||
return false
|
||
}
|
||
_, ips := localAddrs()
|
||
for _, ip := range ips {
|
||
if ip.IsLoopback() || ip.To4() == nil {
|
||
continue
|
||
}
|
||
found := false
|
||
for _, have := range c.IPAddresses {
|
||
if have.Equal(ip) {
|
||
found = true
|
||
break
|
||
}
|
||
}
|
||
if !found {
|
||
return true
|
||
}
|
||
}
|
||
have := map[string]bool{}
|
||
for _, n := range c.DNSNames {
|
||
have[strings.ToLower(n)] = true
|
||
}
|
||
for _, n := range customHostnames() {
|
||
if n = strings.ToLower(strings.TrimSpace(n)); n != "" && !have[n] {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// startHTTPS — слушатель HTTPS рядом с основным HTTP. Ошибка не роняет bj: интерфейс остаётся по HTTP.
|
||
func startHTTPS(rc *RuntimeConfig, handler http.Handler) *http.Server {
|
||
if !rc.Snapshot().HTTPS.Custom && selfSignedStale() {
|
||
log.Printf("https: адреса сервера изменились — новый самоподписанный сертификат")
|
||
_ = ensureSelfSigned(true)
|
||
}
|
||
if err := ensureSelfSigned(false); err != nil {
|
||
log.Printf("https: сертификат не создан: %v — HTTPS выключен", err)
|
||
return nil
|
||
}
|
||
srv := &http.Server{
|
||
Addr: ":" + strconv.Itoa(rc.Snapshot().HTTPS.port()),
|
||
Handler: handler,
|
||
ReadHeaderTimeout: 5 * time.Second,
|
||
TLSConfig: &tls.Config{GetCertificate: loadTLSCert, MinVersion: tls.VersionTLS12},
|
||
}
|
||
ln, err := net.Listen("tcp", srv.Addr)
|
||
if err != nil {
|
||
log.Printf("https: %v — HTTPS выключен, вход по HTTP без перенаправления", err)
|
||
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 {
|
||
httpsUp.Store(false)
|
||
log.Printf("https: %v", err)
|
||
}
|
||
}()
|
||
if rc.Snapshot().HTTPS.StdPorts {
|
||
startStdPorts(srv)
|
||
}
|
||
return srv
|
||
}
|
||
|
||
func registerHTTPS(mux *http.ServeMux, rc *RuntimeConfig) {
|
||
mux.HandleFunc("/admin/setup/https", func(w http.ResponseWriter, r *http.Request) {
|
||
if r.Method != http.MethodPost {
|
||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||
return
|
||
}
|
||
// одна форма раздела: перенаправление, доменное имя, адрес без порта. Применяется только изменённое;
|
||
// порты — последними: bj после них перезапускается.
|
||
h := rc.Snapshot().HTTPS
|
||
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:// выключено.")
|
||
}
|
||
}
|
||
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
|
||
}
|
||
}
|
||
// включено, но порт 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) {
|
||
if r.Method != http.MethodPost {
|
||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||
return
|
||
}
|
||
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) {
|
||
if r.Method != http.MethodPost {
|
||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||
return
|
||
}
|
||
read := func(name string) []byte {
|
||
if f, _, err := r.FormFile(name); err == nil {
|
||
defer f.Close()
|
||
b, _ := io.ReadAll(io.LimitReader(f, 1<<20))
|
||
return b
|
||
}
|
||
return nil
|
||
}
|
||
cert, key := read("cert"), read("key")
|
||
if len(cert) == 0 || len(key) == 0 {
|
||
setupFlash(w, r, "HTTPS: нужны оба файла — сертификат (PEM, с цепочкой) и закрытый ключ (PEM)")
|
||
return
|
||
}
|
||
if _, err := tls.X509KeyPair(cert, key); err != nil {
|
||
setupFlash(w, r, "HTTPS: сертификат и ключ не подходят друг к другу или не в формате PEM: "+err.Error())
|
||
return
|
||
}
|
||
_ = os.MkdirAll(tlsDir, 0o700)
|
||
if err := os.WriteFile(tlsKeyPath(), key, 0o600); err != nil {
|
||
setupFlash(w, r, "HTTPS: "+err.Error())
|
||
return
|
||
}
|
||
if err := os.WriteFile(tlsCertPath(), cert, 0o644); err != nil {
|
||
setupFlash(w, r, "HTTPS: "+err.Error())
|
||
return
|
||
}
|
||
h := rc.Snapshot().HTTPS
|
||
h.Custom = true
|
||
_ = rc.UpdateHTTPS(h)
|
||
cn, until, _ := tlsCertInfo()
|
||
setupFlash(w, r, "HTTPS: установлен сертификат «"+cn+"» до "+until.Format("02.01.2006")+" — действует сразу")
|
||
})
|
||
mux.HandleFunc("/admin/setup/https/selfsigned", func(w http.ResponseWriter, r *http.Request) {
|
||
if r.Method != http.MethodPost {
|
||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||
return
|
||
}
|
||
if err := ensureSelfSigned(true); err != nil {
|
||
setupFlash(w, r, "HTTPS: "+err.Error())
|
||
return
|
||
}
|
||
h := rc.Snapshot().HTTPS
|
||
h.Custom = false
|
||
_ = rc.UpdateHTTPS(h)
|
||
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"
|
||
}
|
||
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"))
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
// 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"
|
||
}
|
||
|
||
// 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()) + "/."
|
||
}
|