// Command bj-artifactory — простой сервер раздачи релизов и обновлений. // // Раскладка хранилища (--root), один подкаталог на канал: // // /stable/manifest.json — подписанный SignedManifest // /stable/bj-server — артефакты, перечисленные в манифесте // /stable/crypto-service.jar // /beta/manifest.json // ... // // HTTP API (потребляет bj-server auto-update и install.sh): // // GET /v1//manifest.json — манифест канала // GET /v1//files/ — артефакт по имени // 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//manifest.json и /v1//files/. 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) }) }