Files
zuevav 8099e50ab2 feat(update): обновления с сервера лицензий по лицензии установки
- по умолчанию https://lic.zetit.ru/updates; клиент артефактории передаёт лицензионный
  токен установки (Authorization: Bearer), сервер отдаёт модули по признакам лицензии
- компоненты получили признак лицензии (ядро и криптосервис — updates, ЛК — web-cabinet,
  НПР — npr, экспорт QUIK — quik-export)
- в bj зашит открытый ключ подписи релизов (закрытый хранится вне сервера лицензий)
- проверено сквозной установкой bj-lk 0.2.0 из канала beta

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 20:43:56 +03:00

132 lines
4.8 KiB
Go

package release
import (
"context"
"crypto/ed25519"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
)
// Client — клиент артефактории: скачивает подписанный манифест и артефакты,
// проверяет подпись зашитым публичным ключом и sha256 каждого файла.
type Client struct {
BaseURL string // напр. https://updates.example.com
Channel string // "stable" | "beta"
Pub ed25519.PublicKey // публичный ключ издателя (зашит в bj-server)
HTTP *http.Client
// Token — лицензионный токен установки (Authorization: Bearer). Сервер обновлений на
// lic.zetit.ru отдаёт файлы только действующим лицензиям, модули — по признакам лицензии.
Token string
}
func (c *Client) authorize(req *http.Request) {
if c.Token != "" {
req.Header.Set("Authorization", "Bearer "+c.Token)
}
}
// NewClient собирает клиент. pubB64 — публичный ключ в base64 (зашитый).
func NewClient(baseURL, channel, pubB64 string) (*Client, error) {
pub, err := ParsePublicKey(pubB64)
if err != nil {
return nil, fmt.Errorf("release: публичный ключ: %w", err)
}
return &Client{
BaseURL: strings.TrimRight(baseURL, "/"),
Channel: channel,
Pub: pub,
HTTP: &http.Client{Timeout: 60 * time.Second},
}, nil
}
// FetchManifest скачивает /v1/<channel>/manifest.json и проверяет подпись.
// Возвращает доверенный Manifest (или ошибку, если подпись не сошлась).
func (c *Client) FetchManifest(ctx context.Context) (*Manifest, error) {
_, m, err := c.FetchSigned(ctx)
return m, err
}
// FetchSigned — подписанный манифест как есть (для повторной проверки root-установщиком) и
// проверенный Manifest.
func (c *Client) FetchSigned(ctx context.Context) (*SignedManifest, *Manifest, error) {
url := fmt.Sprintf("%s/v1/%s/manifest.json", c.BaseURL, c.Channel)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, nil, err
}
c.authorize(req)
resp, err := c.HTTP.Do(req)
if err != nil {
return nil, nil, fmt.Errorf("release: запрос манифеста: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
msg, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
return nil, nil, fmt.Errorf("release: манифест HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(msg)))
}
var sm SignedManifest
if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&sm); err != nil {
return nil, nil, fmt.Errorf("release: разбор манифеста: %w", err)
}
m, err := Verify(&sm, c.Pub) // здесь проверяется подпись
return &sm, m, err
}
// DownloadArtifact скачивает артефакт в destDir, проверяет sha256/размер по
// манифесту, выставляет +x при необходимости. Возвращает путь к файлу.
// Скачивает во временный файл и переименовывает атомарно.
func (c *Client) DownloadArtifact(ctx context.Context, a Artifact, destDir string) (string, error) {
url := fmt.Sprintf("%s/v1/%s/files/%s", c.BaseURL, c.Channel, a.File)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return "", err
}
c.authorize(req)
resp, err := c.HTTP.Do(req)
if err != nil {
return "", fmt.Errorf("release: скачивание %s: %w", a.Name, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
msg, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
return "", fmt.Errorf("release: %s HTTP %d: %s", a.Name, resp.StatusCode, strings.TrimSpace(string(msg)))
}
tmp, err := os.CreateTemp(destDir, "."+a.File+".dl-*")
if err != nil {
return "", err
}
tmpName := tmp.Name()
defer os.Remove(tmpName) // если что-то пойдёт не так — подчистим
if _, err := io.Copy(tmp, resp.Body); err != nil {
tmp.Close()
return "", err
}
if err := tmp.Close(); err != nil {
return "", err
}
// Проверка целостности по манифесту (подпись манифеста уже проверена).
if err := VerifyArtifact(tmpName, a); err != nil {
return "", err
}
mode := os.FileMode(0o644)
if a.Exec {
mode = 0o755
}
if err := os.Chmod(tmpName, mode); err != nil {
return "", err
}
final := filepath.Join(destDir, a.File)
if err := os.Rename(tmpName, final); err != nil { // атомарно в пределах destDir
return "", err
}
return final, nil
}