Files
Bridge-and-Join-s/internal/lkgateway/lkpreview.go
T

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, "Кабинет на этом сервере выключен.")
}
}