3760c1f1dd
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
150 lines
5.4 KiB
Go
150 lines
5.4 KiB
Go
package lkgateway
|
|
|
|
// lkpreview.go — кабинет клиента на этом же сервере, для просмотра.
|
|
//
|
|
// Чтобы посмотреть, как выглядит и работает кабинет, не обязательно ставить его на хостинг:
|
|
// администратор включает его прямо здесь, на отдельном порту ядра. Это тот же встроенный
|
|
// кабинет, что отдаёт страницы ретрансляторам, только с собственным слушателем. Порт
|
|
// открыт по http, поэтому куки выставляются без признака Secure — для просмотра из
|
|
// локальной сети это допустимо, для клиентов из интернета — нет: им кабинет публикуется
|
|
// через хостинг или nginx с сертификатом.
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const lkPreviewDefaultListen = ":8098"
|
|
|
|
var lkPreview struct {
|
|
mu sync.Mutex
|
|
srv *http.Server
|
|
listen string
|
|
err string
|
|
}
|
|
|
|
// lkPreviewApply — привести слушатель в соответствие с настройками: включить, выключить
|
|
// или перезапустить на другом порту.
|
|
func lkPreviewApply(rc *RuntimeConfig) {
|
|
s := rc.Snapshot().LK
|
|
listen := strings.TrimSpace(s.PreviewListen)
|
|
if listen == "" {
|
|
listen = lkPreviewDefaultListen
|
|
}
|
|
lkPreview.mu.Lock()
|
|
defer lkPreview.mu.Unlock()
|
|
if !s.PreviewOn || listen != lkPreview.listen {
|
|
if lkPreview.srv != nil {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
_ = lkPreview.srv.Shutdown(ctx)
|
|
cancel()
|
|
lkPreview.srv, lkPreview.listen = nil, ""
|
|
log.Printf("кабинет: просмотр на этом сервере выключен")
|
|
}
|
|
}
|
|
if !s.PreviewOn || lkPreview.srv != nil {
|
|
lkPreview.err = ""
|
|
return
|
|
}
|
|
if globalLKCabinet == nil {
|
|
lkPreview.err = "встроенный кабинет ещё не запущен"
|
|
return
|
|
}
|
|
cab, err := globalLKCabinet.server()
|
|
if err != nil {
|
|
lkPreview.err = err.Error()
|
|
return
|
|
}
|
|
ln, err := net.Listen("tcp", listen)
|
|
if err != nil {
|
|
lkPreview.err = "порт занят или недоступен: " + err.Error()
|
|
log.Printf("кабинет: просмотр на %s не запущен: %v", listen, err)
|
|
return
|
|
}
|
|
h := cab.Handler()
|
|
srv := &http.Server{
|
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
r.Header.Set("X-BJ-Preview", "1") // куки без Secure: просмотр по http из локальной сети
|
|
h.ServeHTTP(w, r)
|
|
}),
|
|
ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 20 * time.Second, WriteTimeout: 60 * time.Second, IdleTimeout: 60 * time.Second,
|
|
}
|
|
lkPreview.srv, lkPreview.listen, lkPreview.err = srv, listen, ""
|
|
go func() {
|
|
if err := srv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
log.Printf("кабинет: просмотр на %s остановлен: %v", listen, err)
|
|
lkPreview.mu.Lock()
|
|
if lkPreview.srv == srv {
|
|
lkPreview.srv, lkPreview.listen, lkPreview.err = nil, "", err.Error()
|
|
}
|
|
lkPreview.mu.Unlock()
|
|
}
|
|
}()
|
|
log.Printf("кабинет: просмотр на этом сервере включён, %s", listen)
|
|
}
|
|
|
|
// LKPreviewStatus — для страницы настроек.
|
|
type LKPreviewStatus struct {
|
|
On bool
|
|
Listen string
|
|
URL string // адрес для администратора, по имени, с которого он открыл настройки
|
|
Err string
|
|
}
|
|
|
|
func lkPreviewStatus(rc *RuntimeConfig, r *http.Request) LKPreviewStatus {
|
|
s := rc.Snapshot().LK
|
|
listen := strings.TrimSpace(s.PreviewListen)
|
|
if listen == "" {
|
|
listen = lkPreviewDefaultListen
|
|
}
|
|
lkPreview.mu.Lock()
|
|
st := LKPreviewStatus{On: lkPreview.srv != nil, Listen: listen, Err: lkPreview.err}
|
|
lkPreview.mu.Unlock()
|
|
host := r.Host
|
|
if h, _, err := net.SplitHostPort(host); err == nil {
|
|
host = h
|
|
}
|
|
_, port, err := net.SplitHostPort(listen)
|
|
if err != nil {
|
|
port = strings.TrimPrefix(listen, ":")
|
|
}
|
|
st.URL = "http://" + host + ":" + port + "/"
|
|
return st
|
|
}
|
|
|
|
// lkPreviewSave — POST /admin/setup/lk/preview: включить или выключить просмотр.
|
|
func (h *setupHandlers) lkPreviewSave(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
s := h.rc.Snapshot().LK
|
|
s.PreviewOn = r.FormValue("preview_on") == "1"
|
|
if v := strings.TrimSpace(r.FormValue("preview_listen")); v != "" {
|
|
if !strings.Contains(v, ":") {
|
|
v = ":" + v
|
|
}
|
|
s.PreviewListen = v
|
|
}
|
|
if err := h.rc.UpdateLK(s); err != nil {
|
|
setupFlash(w, r, "Кабинет: ошибка сохранения: "+err.Error())
|
|
return
|
|
}
|
|
lkPreviewApply(h.rc)
|
|
st := lkPreviewStatus(h.rc, r)
|
|
switch {
|
|
case st.Err != "":
|
|
setupFlash(w, r, "Кабинет на этом сервере не запущен: "+st.Err)
|
|
case st.On:
|
|
setupFlash(w, r, "Кабинет запущен на этом сервере: "+st.URL+" — вход по учётным записям из карточки «Пользователи кабинета».")
|
|
default:
|
|
setupFlash(w, r, "Кабинет на этом сервере выключен.")
|
|
}
|
|
}
|