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