package lkgateway // Модуль «Экспорт биржевой информации» (ARQA quikexport) работает отдельной службой bj-quikexport // (modules/quikexport, обновляется независимо). Раздел остаётся на странице «Настройка»: bj-server // получает от службы готовый HTML раздела и цвет индикатора меню, а формы раздела // (/admin/setup/quikexport/…) проксирует службе и превращает её ответ X-BJ-Flash в обычное // сообщение страницы настроек. Лицензию проверяет bj-server и передаёт в X-BJ-Licensed. import ( "bytes" "context" "fmt" "html/template" "io" "net/http" "net/url" "os" "strings" "time" ) // quikExportFeature — лицензионный признак модуля. const quikExportFeature = "quik-export" // Применение настроек ждёт root-воркер до 3 минут. var quikExportHTTP = &http.Client{Timeout: 4 * time.Minute, CheckRedirect: noRedirect} func quikExportURL() string { if u := strings.TrimSpace(os.Getenv("BJ_QUIKEXPORT_URL")); u != "" { return strings.TrimRight(u, "/") } return "http://127.0.0.1:8084" } // QuikExportView — раздел «Экспорт QUIK» на странице настроек. type QuikExportView struct { HTML template.HTML Ind string // индикатор в меню: ok | warn | err | пусто } func quikExportDo(ctx context.Context, rc *RuntimeConfig, method, uri string, body io.Reader, contentType string) (*http.Response, error) { key, err := moduleKey() if err != nil { return nil, fmt.Errorf("служебный ключ модулей: %w", err) } req, err := http.NewRequestWithContext(ctx, method, quikExportURL()+uri, body) if err != nil { return nil, err } if contentType != "" { req.Header.Set("Content-Type", contentType) } req.Header.Set("X-BJ-Module-Key", key) lic := "0" if moduleAllowed(rc, quikExportFeature) { lic = "1" } req.Header.Set("X-BJ-Licensed", lic) return quikExportHTTP.Do(req) } func quikExportView(rc *RuntimeConfig, showRO bool) QuikExportView { uri := "/section" if showRO { uri += "?reveal_ro=1" } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() resp, err := quikExportDo(ctx, rc, http.MethodGet, uri, nil, "") if err == nil { b, rerr := io.ReadAll(io.LimitReader(resp.Body, 4<<20)) resp.Body.Close() switch { case rerr != nil: err = rerr case resp.StatusCode != http.StatusOK: err = fmt.Errorf("служба ответила %d: %s", resp.StatusCode, strings.TrimSpace(string(b))) default: return QuikExportView{HTML: template.HTML(b), Ind: resp.Header.Get("X-BJ-Indicator")} } } return QuikExportView{Ind: "err", HTML: template.HTML(`

QUIK — Экспорт биржевой информации

`) + moduleMessage( "Служба модуля не отвечает", "Служба bj-quikexport не запущена или ещё не установлена на этом сервере ("+err.Error()+").", "Установка и обновление модуля: Настройка → Обновления. Состояние службы: systemctl status bj-quikexport. Сам экспортёр ARQA от этого не зависит и продолжает выгрузку.")} } // registerQuikExportSetup — формы раздела проксируются службе bj-quikexport. func registerQuikExportSetup(mux *http.ServeMux, h *setupHandlers) { proxy := func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(io.LimitReader(r.Body, 8<<20)) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if len(body) == 0 && r.PostForm != nil && !strings.HasPrefix(r.Header.Get("Content-Type"), "multipart/") { body = []byte(r.PostForm.Encode()) // форму уже разобрали раньше } // setupFlash читает return_to/section из формы — возвращаем тело на место. restore := func() { r.Body = io.NopCloser(bytes.NewReader(body)) } resp, err := quikExportDo(r.Context(), h.rc, r.Method, r.URL.RequestURI(), bytes.NewReader(body), r.Header.Get("Content-Type")) if err != nil { if r.Method == http.MethodPost { restore() setupFlash(w, r, "Экспорт QUIK: служба bj-quikexport не отвечает — "+err.Error()) return } http.Error(w, "служба bj-quikexport не отвечает: "+err.Error(), http.StatusServiceUnavailable) return } defer resp.Body.Close() if f := resp.Header.Get("X-BJ-Flash"); f != "" { msg, uerr := url.PathUnescape(f) if uerr != nil { msg = f } restore() setupFlash(w, r, msg) return } for _, k := range []string{"Content-Type", "Content-Disposition", "Cache-Control"} { if v := resp.Header.Get(k); v != "" { w.Header().Set(k, v) } } w.WriteHeader(resp.StatusCode) _, _ = io.Copy(w, resp.Body) } mux.HandleFunc("/admin/setup/quikexport", proxy) mux.HandleFunc("/admin/setup/quikexport/", proxy) }