package lk // envelope.go — отрисовка страниц в ядре и тонкий ретранслятор. // // Страницы кабинета может собирать не программа на хостинге, а само ядро: у него все // данные и шаблоны. Тогда на хостинге остаётся ретранслятор — он упаковывает запрос // браузера в конверт (метод, путь, заголовки, тело), передаёт ядру и отдаёт браузеру // ответ из конверта (код, заголовки, тело). Ретранслятор ничего не разбирает и ничем // не управляет, поэтому его легко держать и на Go для своего сервера, и на PHP для // обычного хостинга — вся логика и защита живут в ядре в одном экземпляре. // // Этот же пакет, собранный внутрь ядра, разворачивает конверт обратно в http.Request и // прогоняет его через свой обычный обработчик страниц (ServeEnvelope). Данные пакет // берёт через тот же API /api/lk/*, что и снаружи, только вызовом внутри процесса. import ( "bytes" "context" "encoding/json" "io" "net/http" "net/url" "strings" "time" ) // PageRequest — запрос браузера в конверте. type PageRequest struct { ID string `json:"id,omitempty"` Kind string `json:"kind"` // page — страница кабинета; пусто — вызов API (старый кабинет) Method string `json:"method"` Path string `json:"path"` // путь с параметрами запроса Host string `json:"host,omitempty"` Scheme string `json:"scheme,omitempty"` // http | https — как браузер открыл кабинет Headers map[string][]string `json:"headers,omitempty"` Body []byte `json:"body,omitempty"` Session string `json:"session,omitempty"` // только для вызовов API ClientIP string `json:"client_ip,omitempty"` ClientAgent string `json:"client_agent,omitempty"` } // PageResponse — ответ ядра в конверте. type PageResponse struct { ID string `json:"id,omitempty"` Status int `json:"status"` Headers map[string][]string `json:"headers,omitempty"` Body []byte `json:"body,omitempty"` } // forwardHeaders — заголовки браузера, которые ретранслятор передаёт ядру. // Остальное (Referer, Origin, служебные заголовки хостинга) ядру не нужно. var forwardHeaders = []string{"Cookie", "Accept", "Accept-Language", "Content-Type", "If-None-Match", "If-Modified-Since"} // hopHeaders — заголовки соединения: их ретранслятор не переносит ни в одну сторону. var hopHeaders = map[string]bool{"Connection": true, "Keep-Alive": true, "Transfer-Encoding": true, "Upgrade": true, "Proxy-Authenticate": true, "Proxy-Authorization": true, "Te": true, "Trailer": true, "Content-Length": true} // envelopeFrom — конверт из запроса браузера (ретранслятор). func envelopeFrom(r *http.Request, maxBody int64) (PageRequest, error) { body, err := io.ReadAll(io.LimitReader(r.Body, maxBody+1)) if err != nil { return PageRequest{}, err } if int64(len(body)) > maxBody { return PageRequest{}, errRequestTooLarge } scheme := "http" if httpsRequest(r) { scheme = "https" } env := PageRequest{Kind: "page", Method: r.Method, Path: r.URL.RequestURI(), Host: r.Host, Scheme: scheme, Headers: map[string][]string{}, Body: body, ClientIP: clientIP(r), ClientAgent: r.UserAgent()} for _, h := range forwardHeaders { if v := r.Header.Values(h); len(v) > 0 { env.Headers[h] = v } } return env, nil } // writeEnvelope — ответ ядра браузеру (ретранслятор). func writeEnvelope(w http.ResponseWriter, res PageResponse) { for k, vs := range res.Headers { if hopHeaders[http.CanonicalHeaderKey(k)] { continue } for _, v := range vs { w.Header().Add(k, v) } } if res.Status == 0 { res.Status = http.StatusBadGateway } w.WriteHeader(res.Status) _, _ = w.Write(res.Body) } // ServeEnvelope — развернуть конверт и выполнить запрос обычным обработчиком страниц (ядро). func (s *Server) ServeEnvelope(ctx context.Context, env PageRequest) PageResponse { if env.Method == "" { env.Method = http.MethodGet } u, err := url.ParseRequestURI(env.Path) if err != nil || strings.HasPrefix(env.Path, "//") { return PageResponse{ID: env.ID, Status: http.StatusBadRequest, Body: []byte("некорректный путь")} } cctx, cancel := context.WithTimeout(ctx, 40*time.Second) defer cancel() req, err := http.NewRequestWithContext(cctx, env.Method, u.String(), bytes.NewReader(env.Body)) if err != nil { return PageResponse{ID: env.ID, Status: http.StatusBadRequest, Body: []byte("некорректный запрос")} } for k, vs := range env.Headers { ck := http.CanonicalHeaderKey(k) if hopHeaders[ck] || strings.HasPrefix(ck, "X-Bj-") || strings.HasPrefix(ck, "X-Forwarded-") || ck == "X-Real-Ip" { continue // служебные заголовки ставим сами, чужие не пропускаем } for _, v := range vs { req.Header.Add(ck, v) } } if env.Host != "" { req.Host = env.Host } if env.Scheme == "https" { req.Header.Set("X-Forwarded-Proto", "https") // куки Secure, HSTS — как при прямом TLS } if env.ClientIP != "" { req.Header.Set("X-Real-IP", env.ClientIP) req.RemoteAddr = env.ClientIP + ":0" } if env.ClientAgent != "" { req.Header.Set("User-Agent", env.ClientAgent) } rec := &envelopeRecorder{status: http.StatusOK, hdr: http.Header{}} s.Handler().ServeHTTP(rec, req) return PageResponse{ID: env.ID, Status: rec.status, Headers: rec.hdr, Body: rec.buf.Bytes()} } type envelopeRecorder struct { status int hdr http.Header buf bytes.Buffer wrote bool } func (w *envelopeRecorder) Header() http.Header { return w.hdr } func (w *envelopeRecorder) Write(b []byte) (int, error) { w.wrote = true return w.buf.Write(b) } func (w *envelopeRecorder) WriteHeader(code int) { if !w.wrote { w.status = code } } // relayMaxBody — сколько ретранслятор принимает от браузера (формы кабинета маленькие). const relayMaxBody = 1 << 20 // relay — обработчик ретранслятора: конверт → ядро → ответ браузеру. func (s *Server) relay(w http.ResponseWriter, r *http.Request) { env, err := envelopeFrom(r, relayMaxBody) if err != nil { http.Error(w, "Запрос слишком большой", http.StatusRequestEntityTooLarge) return } res, err := s.bj().page(reqCtx(r), env) if err != nil { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusBadGateway) s.tpl.ExecuteTemplate(w, "relay_error", map[string]any{"Title": s.conf().Title, "Error": err.Error()}) //nolint:errcheck return } writeEnvelope(w, res) } // page — доставить конверт ядру: напрямую (POST /api/lk/render) или через очередь режима ожидания. func (b *bjClient) page(ctx context.Context, env PageRequest) (PageResponse, error) { if b.agent != nil { res, err := b.agent.pushEnvelope(ctx, env, 40*time.Second) if err != nil { return PageResponse{}, err } return res, nil } raw, _ := json.Marshal(env) req, err := http.NewRequestWithContext(ctx, http.MethodPost, b.cfg.BJURL+"/api/lk/render", bytes.NewReader(raw)) if err != nil { return PageResponse{}, err } req.Header.Set("X-BJ-Key", b.cfg.BJKey) req.Header.Set("Content-Type", "application/json") resp, err := b.hc.Do(req) if err != nil { return PageResponse{}, errNoCore(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { var e struct{ Error string } _ = json.NewDecoder(io.LimitReader(resp.Body, 64<<10)).Decode(&e) if e.Error == "" { e.Error = "ядро ответило " + resp.Status } return PageResponse{}, errCore(e.Error) } var out PageResponse if err := json.NewDecoder(io.LimitReader(resp.Body, 32<<20)).Decode(&out); err != nil { return PageResponse{}, errCore("ответ ядра не разобран") } return out, nil }