Кабинет: страницы собирает ядро, на хостинге — ретранслятор (Go и PHP)
- modules/lk: конверт запроса/ответа (PageRequest/PageResponse), ServeEnvelope — тот же обработчик страниц внутри ядра; клиент к API внутри процесса (Config.Local/LocalKey); режим ретранслятора (Config.Relay, BJ_RENDER=core, выбор в установщике и lk.json) - ядро: встроенный кабинет (lkcabinet.go), POST /api/lk/render для прямого режима, цикл режима ожидания выполняет конверты страниц; API через ретранслятор недостижимо - PHP-ретранслятор для обычного хостинга (modules/lk/relay-php: index.php, .htaccess, README): установщик в браузере, прямой режим через curl, режим ожидания через очередь в SQLite (BEGIN IMMEDIATE, busy_timeout), конфиг в bj-lk-data рядом с сайтом или в data под .htaccess; архив со страницы настроек (/admin/setup/lk/relay-php.zip) - настройки: карточка НПР/УДС перенесена из раздела «Лицензия» в «Экспорт QUIK»; кнопка «Обновить с сервера лицензий» видна при любом ключе - инструкция /admin/help/lk: два варианта размещения, шаги для PHP-хостинга - тесты: TestLKRelayRendersInCore (оба режима), TestLKPHPRelayAgentMode (настоящий PHP против цикла ядра, PHP_BIN), проверка безопасности через PHP-ретранслятор 34/34 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
This commit is contained in:
+59
-10
@@ -11,6 +11,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -34,6 +35,12 @@ type Config struct {
|
||||
// Agent — режим ожидания: у ядра нет адреса, доступного кабинету, поэтому
|
||||
// не кабинет ходит в ядро, а ядро само подключается сюда за запросами.
|
||||
Agent bool
|
||||
// Relay — ретранслятор: страницы рисует ядро, здесь только передача конвертов.
|
||||
Relay bool
|
||||
// Local — кабинет собран внутрь ядра: API вызывается в том же процессе, ключ
|
||||
// берётся функцией LocalKey (он меняется в настройках без перезапуска).
|
||||
Local http.Handler
|
||||
LocalKey func() string
|
||||
}
|
||||
|
||||
// Server — кабинет. Настройки живут за мьютексом: веб-установщик меняет их на ходу,
|
||||
@@ -45,6 +52,9 @@ type Server struct {
|
||||
agent *agentQueue
|
||||
dataDir string // где лежит lk.json (веб-установщик)
|
||||
tpl *template.Template
|
||||
|
||||
handlerOnce sync.Once
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
// New — кабинет с разобранными шаблонами.
|
||||
@@ -124,7 +134,7 @@ func (s *Server) apply(cfg Config) {
|
||||
if cfg.Agent {
|
||||
s.agent = newAgentQueue()
|
||||
}
|
||||
s.bjc = &bjClient{cfg: cfg, hc: &http.Client{Timeout: 20 * time.Second}, agent: s.agent}
|
||||
s.bjc = &bjClient{cfg: cfg, hc: &http.Client{Timeout: 20 * time.Second}, agent: s.agent, local: cfg.Local, localKey: cfg.LocalKey}
|
||||
}
|
||||
|
||||
// conf — текущие настройки (копия).
|
||||
@@ -151,6 +161,9 @@ func (s *Server) agentQueue() *agentQueue {
|
||||
func (s *Server) Configured() bool { return s.conf().configured() }
|
||||
|
||||
func (c Config) configured() bool {
|
||||
if c.Local != nil {
|
||||
return true // внутри ядра настраивать нечего
|
||||
}
|
||||
if c.BJKey == "" {
|
||||
return false
|
||||
}
|
||||
@@ -175,9 +188,11 @@ func (c Config) insecureCore() bool {
|
||||
// ---- клиент к bj-server ----
|
||||
|
||||
type bjClient struct {
|
||||
cfg Config
|
||||
hc *http.Client
|
||||
agent *agentQueue // задан в режиме ожидания
|
||||
cfg Config
|
||||
hc *http.Client
|
||||
agent *agentQueue // задан в режиме ожидания
|
||||
local http.Handler // задан внутри ядра: API вызывается без сети
|
||||
localKey func() string
|
||||
}
|
||||
|
||||
func (b *bjClient) call(ctx context.Context, method, path, session string, body any, out any) error {
|
||||
@@ -189,11 +204,19 @@ func (b *bjClient) call(ctx context.Context, method, path, session string, body
|
||||
return b.callAgent(ctx, method, path, session, raw, out)
|
||||
}
|
||||
rdr := bytes.NewReader(raw)
|
||||
req, err := http.NewRequestWithContext(ctx, method, b.cfg.BJURL+path, rdr)
|
||||
target := b.cfg.BJURL + path
|
||||
if b.local != nil {
|
||||
target = path
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, target, rdr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("X-BJ-Key", b.cfg.BJKey)
|
||||
key := b.cfg.BJKey
|
||||
if b.localKey != nil {
|
||||
key = b.localKey()
|
||||
}
|
||||
req.Header.Set("X-BJ-Key", key)
|
||||
if session != "" {
|
||||
req.Header.Set("X-BJ-Session", session)
|
||||
}
|
||||
@@ -206,9 +229,18 @@ func (b *bjClient) call(ctx context.Context, method, path, session string, body
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
resp, err := b.hc.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("нет связи с сервером: %w", err)
|
||||
var resp *http.Response
|
||||
if b.local != nil {
|
||||
// Внутри ядра: тот же обработчик API, только без сети. Граница проверок та же.
|
||||
rec := &envelopeRecorder{status: http.StatusOK, hdr: http.Header{}}
|
||||
req.RemoteAddr = "127.0.0.1:0"
|
||||
b.local.ServeHTTP(rec, req)
|
||||
resp = &http.Response{StatusCode: rec.status, Header: rec.hdr, Body: io.NopCloser(&rec.buf)}
|
||||
} else {
|
||||
resp, err = b.hc.Do(req)
|
||||
if err != nil {
|
||||
return errNoCore(err)
|
||||
}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= 400 {
|
||||
@@ -225,6 +257,12 @@ func (b *bjClient) call(ctx context.Context, method, path, session string, body
|
||||
return nil
|
||||
}
|
||||
|
||||
// errNoCore / errCore — ошибки связи с ядром одним текстом для страниц.
|
||||
func errNoCore(err error) error { return fmt.Errorf("нет связи с сервером: %w", err) }
|
||||
func errCore(msg string) error { return fmt.Errorf("%s", msg) }
|
||||
|
||||
var errRequestTooLarge = fmt.Errorf("запрос слишком большой")
|
||||
|
||||
// callAgent — тот же запрос, но через очередь: его заберёт ядро, когда подключится.
|
||||
func (b *bjClient) callAgent(ctx context.Context, method, path, session string, raw []byte, out any) error {
|
||||
res, err := b.agent.push(ctx, method, path, raw, session, 40*time.Second)
|
||||
@@ -512,6 +550,11 @@ func session(r *http.Request) string {
|
||||
// Handler — все страницы кабинета. Пока кабинет не настроен, любая страница ведёт
|
||||
// в веб-установщик (/install), как у обычных веб-приложений после копирования файлов.
|
||||
func (s *Server) Handler() http.Handler {
|
||||
s.handlerOnce.Do(func() { s.handler = s.buildHandler() })
|
||||
return s.handler
|
||||
}
|
||||
|
||||
func (s *Server) buildHandler() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
s.registerInstall(mux)
|
||||
|
||||
@@ -977,10 +1020,16 @@ func (s *Server) Handler() http.Handler {
|
||||
s.registerAgent(mux)
|
||||
mux.Handle("/assets/", assetsHandler())
|
||||
return harden(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.Configured() && !strings.HasPrefix(r.URL.Path, "/install") && !strings.HasPrefix(r.URL.Path, "/assets/") && r.URL.Path != "/health" {
|
||||
p := r.URL.Path
|
||||
if !s.Configured() && !strings.HasPrefix(p, "/install") && !strings.HasPrefix(p, "/assets/") && p != "/health" {
|
||||
http.Redirect(w, r, "/install", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
// Ретранслятор: всё, кроме своих служебных страниц, уезжает в ядро конвертом.
|
||||
if s.conf().Relay && !strings.HasPrefix(p, "/install") && !strings.HasPrefix(p, "/agent/") && p != "/health" {
|
||||
s.relay(w, r)
|
||||
return
|
||||
}
|
||||
mux.ServeHTTP(w, r)
|
||||
}))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user