Кабинет: страницы собирает ядро, на хостинге — ретранслятор (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:
@@ -3,8 +3,10 @@ package lkgateway
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -88,3 +90,94 @@ func TestLKAgentRejectsForeignPath(t *testing.T) {
|
||||
t.Fatalf("чужой путь принят: %+v", task)
|
||||
}
|
||||
}
|
||||
|
||||
// Ретранслятор: страницы собирает ядро. Прямой режим — конверт через POST /api/lk/render,
|
||||
// режим ожидания — ядро забирает конверт из очереди ретранслятора. В обоих случаях браузер
|
||||
// получает страницу входа с защитными заголовками и кукой, выставленными ядром.
|
||||
func TestLKRelayRendersInCore(t *testing.T) {
|
||||
const key = "lk-relay-test-key"
|
||||
rc, err := NewRuntimeConfig(t.TempDir() + "/setup.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lkCfg := rc.Snapshot().LK
|
||||
lkCfg.APIKey = key
|
||||
if err := rc.UpdateLK(lkCfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
registerLKAPI(mux, rc, nil)
|
||||
coreSrv := httptest.NewServer(mux)
|
||||
defer coreSrv.Close()
|
||||
|
||||
check := func(name string, relay *lk.Server) {
|
||||
t.Helper()
|
||||
rs := httptest.NewServer(relay.Handler())
|
||||
defer rs.Close()
|
||||
client := &http.Client{Timeout: 20 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
|
||||
req, _ := http.NewRequest(http.MethodGet, rs.URL+"/login", nil)
|
||||
req.Header.Set("User-Agent", "Проверочный браузер")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", name, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK || !strings.Contains(string(body), "Логин или номер договора") {
|
||||
t.Fatalf("%s: код %d, тело %.200s", name, resp.StatusCode, body)
|
||||
}
|
||||
if resp.Header.Get("Content-Security-Policy") == "" || resp.Header.Get("X-Frame-Options") != "DENY" {
|
||||
t.Errorf("%s: защитные заголовки ядра не дошли до браузера", name)
|
||||
}
|
||||
var csrf bool
|
||||
for _, c := range resp.Cookies() {
|
||||
if c.Name == "bj_lk_csrf" && c.HttpOnly {
|
||||
csrf = true
|
||||
}
|
||||
}
|
||||
if !csrf {
|
||||
t.Errorf("%s: кука формы от ядра не дошла", name)
|
||||
}
|
||||
// API ядра через ретранслятор недостижимо: ядро считает это страницей и отвечает 404
|
||||
resp2, err := client.Get(rs.URL + "/api/lk/ping")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp2.Body.Close()
|
||||
if resp2.StatusCode != http.StatusNotFound {
|
||||
t.Errorf("%s: /api/lk/ping через ретранслятор: код %d, ожидали 404", name, resp2.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
direct, err := lk.New(lk.Config{BJKey: key, BJURL: coreSrv.URL, Relay: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
check("прямой режим", direct)
|
||||
|
||||
agent, err := lk.New(lk.Config{BJKey: key, Agent: true, Relay: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
agentSrv := httptest.NewServer(agent.Handler())
|
||||
defer agentSrv.Close()
|
||||
lkCfg.AgentMode, lkCfg.CabinetURL = true, agentSrv.URL
|
||||
if err := rc.UpdateLK(lkCfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
core := &Server{rc: rc, mux: mux}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
go core.lkAgentLoop(ctx)
|
||||
// у режима ожидания ретранслятор уже запущен как agentSrv — проверяем через него
|
||||
client := &http.Client{Timeout: 20 * time.Second}
|
||||
resp, err := client.Get(agentSrv.URL + "/login")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK || !strings.Contains(string(body), "Логин или номер договора") {
|
||||
t.Fatalf("режим ожидания: код %d, тело %.200s", resp.StatusCode, body)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user