648a5ae05f
- Дистрибутив ИШ — компонент выпуска igate-dist (версия = версия ИШ). В выпуск попадает только одобренный архив: deploy/release/igate-approved (версия и sha256), сборка сверяет контрольную сумму. Клиент получает его обновлением; bj-server после обновления (и при запуске) сам обновляет ИШ, если на сервере он старее; итог — событие в «Новостях». Настройки, каналы и база ИШ сохраняются, прежняя версия — в /var/backups. - Установка «с сервера» берёт самый новый проверенный дистрибутив: с образа или из обновления. С сайта НРД — только в режиме разработки. - Большие файлы: одна прежняя копия вместо пяти; предел скачивания — по размеру. - Ход установки ИШ — этапами: пройденные, текущий («идёт 0:41 · обычно до 1:30», дольше обычного — предупреждение), журнал под раскрытием; кнопка на время установки выключена. - Установщик: уникальные этапы; igate-cli и сессия ключницы с принудительным завершением (timeout -k); зависшая установка (ход не менялся 15 минут) снимается повторным запуском; служба установки — не дольше 40 минут. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
139 lines
5.1 KiB
Go
139 lines
5.1 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)
|
|
hc := c.HTTP
|
|
if hc.Timeout > 0 && a.Size > 16<<20 {
|
|
// большой файл (дистрибутив ИШ — 160 МБ): общий предел запроса — по размеру, из расчёта 256 КБ/с
|
|
cp := *hc
|
|
cp.Timeout = hc.Timeout + time.Duration(a.Size/(256<<10))*time.Second
|
|
hc = &cp
|
|
}
|
|
resp, err := hc.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
|
|
}
|