9737c787f9
Инфраструктура M2M (живой обмен с НРД через ИШ): - обработка M2MTransferResponse: ERROR(M2Mxx) → заявка Отклонена, сохранение ответа; INFO → ждём Decision; идемпотентность поллера - fallback-корреляция ответов с нулевым GUID (M2M14/M2M17) по FIFO - сырой XML ответа НРД в карточке заявки (для пересылки в ТП) - тестовый пакет роботу приведён к эталону m2m_robot_samples (CostInfo=Yes, 4 бумаги, IsolationStatus, DocumentSeries=сценарий); override паспорта - редирект из теста сразу в карточку заявки Мастер установки ключа Валидаты на флешку (admin/setup/keywizard): - пошаговый: загрузка .7z+пароль → выбор флешки → запись → справочник сертификатов (CRL) → перезапуск+проверка ИШ → готово - привилегированный воркер (bj-keymedia) в host-namespace через файл-обмен, bj-server остаётся в песочнице - сохранение структуры профиля архива (spr<N>), перечисление съёмных USB Прочее: - пакет-доказательство для ТП НРД + форма регистрации участника M2M - эталонные образцы робота (DOC/m2m_robot_samples) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
115 lines
3.6 KiB
Go
115 lines
3.6 KiB
Go
// Command bj-artifactory — простой сервер раздачи релизов и обновлений.
|
|
//
|
|
// Раскладка хранилища (--root), один подкаталог на канал:
|
|
//
|
|
// <root>/stable/manifest.json — подписанный SignedManifest
|
|
// <root>/stable/bj-server — артефакты, перечисленные в манифесте
|
|
// <root>/stable/crypto-service.jar
|
|
// <root>/beta/manifest.json
|
|
// ...
|
|
//
|
|
// HTTP API (потребляет bj-server auto-update и install.sh):
|
|
//
|
|
// GET /v1/<channel>/manifest.json — манифест канала
|
|
// GET /v1/<channel>/files/<name> — артефакт по имени
|
|
// GET /healthz — проверка живости
|
|
//
|
|
// Подпись манифеста делает bj-release; здесь только статическая раздача.
|
|
// Перед прод-выкаткой ставится за TLS-reverse-proxy (nginx, см.
|
|
// deploy/artifactory/nginx.conf).
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func main() {
|
|
addr := flag.String("addr", ":8090", "адрес прослушивания")
|
|
root := flag.String("root", "./releases", "корень хранилища релизов")
|
|
flag.Parse()
|
|
|
|
abs, err := filepath.Abs(*root)
|
|
if err != nil {
|
|
log.Fatalf("bj-artifactory: root: %v", err)
|
|
}
|
|
if _, err := os.Stat(abs); err != nil {
|
|
log.Fatalf("bj-artifactory: каталог релизов %s недоступен: %v", abs, err)
|
|
}
|
|
srv := &server{root: abs}
|
|
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("ok")) })
|
|
mux.HandleFunc("/v1/", srv.handleV1)
|
|
|
|
log.Printf("bj-artifactory: раздаю %s на %s", abs, *addr)
|
|
httpSrv := &http.Server{Addr: *addr, Handler: logging(mux), ReadHeaderTimeout: 10 * time.Second}
|
|
log.Fatal(httpSrv.ListenAndServe())
|
|
}
|
|
|
|
type server struct{ root string }
|
|
|
|
// handleV1 разбирает /v1/<channel>/manifest.json и /v1/<channel>/files/<name>.
|
|
func (s *server) handleV1(w http.ResponseWriter, r *http.Request) {
|
|
rest := strings.TrimPrefix(r.URL.Path, "/v1/")
|
|
parts := strings.SplitN(rest, "/", 3)
|
|
if len(parts) < 2 {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
channel := parts[0]
|
|
if !safeName(channel) {
|
|
http.Error(w, "bad channel", http.StatusBadRequest)
|
|
return
|
|
}
|
|
switch {
|
|
case len(parts) == 2 && parts[1] == "manifest.json":
|
|
s.serveFile(w, r, filepath.Join(s.root, channel, "manifest.json"), "application/json")
|
|
case len(parts) == 3 && parts[1] == "files":
|
|
name := parts[2]
|
|
if !safeName(name) {
|
|
http.Error(w, "bad name", http.StatusBadRequest)
|
|
return
|
|
}
|
|
s.serveFile(w, r, filepath.Join(s.root, channel, name), "application/octet-stream")
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}
|
|
|
|
func (s *server) serveFile(w http.ResponseWriter, r *http.Request, path, ctype string) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
defer f.Close()
|
|
fi, err := f.Stat()
|
|
if err != nil || fi.IsDir() {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", ctype)
|
|
http.ServeContent(w, r, filepath.Base(path), fi.ModTime(), f)
|
|
}
|
|
|
|
// safeName запрещает обход каталогов (.., /, пустые).
|
|
func safeName(s string) bool {
|
|
if s == "" || s == "." || s == ".." {
|
|
return false
|
|
}
|
|
return !strings.ContainsAny(s, "/\\") && !strings.Contains(s, "..")
|
|
}
|
|
|
|
func logging(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
next.ServeHTTP(w, r)
|
|
log.Printf("%s %s %s", r.RemoteAddr, r.Method, r.URL.Path)
|
|
})
|
|
}
|