Кабинет клиента на этом сервере для просмотра: включается в настройках, отдельный порт (:8098), куки без Secure только для этого слушателя
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
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, "Кабинет на этом сервере выключен.")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user