feat: живой цикл M2M с НРД + мастер установки ключа на флешку
Инфраструктура 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>
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
// 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)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user