diff --git a/knotserver/git/git.go b/knotserver/git/git.go index 959f3d46..c7490000 100644 --- a/knotserver/git/git.go +++ b/knotserver/git/git.go @@ -37,7 +37,8 @@ func init() { } var ( - ErrBinaryFile = fmt.Errorf("binary file") + ErrBinaryFile = fmt.Errorf("binary file") + ErrNotBinaryFile = fmt.Errorf("not binary file") ) type GitRepo struct { @@ -193,6 +194,35 @@ func (g *GitRepo) FileContent(path string) (string, error) { } } +func (g *GitRepo) BinContent(path string) ([]byte, error) { + c, err := g.r.CommitObject(g.h) + if err != nil { + return nil, fmt.Errorf("commit object: %w", err) + } + + tree, err := c.Tree() + if err != nil { + return nil, fmt.Errorf("file tree: %w", err) + } + + file, err := tree.File(path) + if err != nil { + return nil, err + } + + isbin, _ := file.IsBinary() + if isbin { + reader, err := file.Reader() + if err != nil { + return nil, fmt.Errorf("opening file reader: %w", err) + } + defer reader.Close() + + return io.ReadAll(reader) + } + return nil, ErrNotBinaryFile +} + func (g *GitRepo) Tags() ([]*TagReference, error) { iter, err := g.r.Tags() if err != nil { diff --git a/knotserver/handler.go b/knotserver/handler.go index 10978740..8ced4615 100644 --- a/knotserver/handler.go +++ b/knotserver/handler.go @@ -100,6 +100,7 @@ func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, j r.Route("/blob/{ref}", func(r chi.Router) { r.Get("/*", h.Blob) + r.Get("/raw/*", h.BlobRaw) }) r.Get("/log/{ref}", h.Log) diff --git a/knotserver/routes.go b/knotserver/routes.go index 10c05355..bf97eee5 100644 --- a/knotserver/routes.go +++ b/knotserver/routes.go @@ -194,12 +194,47 @@ func (h *Handle) RepoTree(w http.ResponseWriter, r *http.Request) { return } +func (h *Handle) BlobRaw(w http.ResponseWriter, r *http.Request) { + treePath := chi.URLParam(r, "*") + ref := chi.URLParam(r, "ref") + ref, _ = url.PathUnescape(ref) + + l := h.l.With("handler", "BlobRaw", "ref", ref, "treePath", treePath) + + path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) + gr, err := git.Open(path, ref) + if err != nil { + notFound(w) + return + } + + contents, err := gr.BinContent(treePath) + if err != nil { + writeError(w, err.Error(), http.StatusBadRequest) + l.Error("file content", "error", err.Error()) + return + } + + mimeType := http.DetectContentType(contents) + + if !strings.HasPrefix(mimeType, "image/") && !strings.HasPrefix(mimeType, "video/") { + l.Error("attempted to serve non-image/video file", "mimetype", mimeType) + writeError(w, "only image and video files can be accessed directly", http.StatusForbidden) + return + } + + w.Header().Set("Cache-Control", "public, max-age=86400") // cache for 24 hours + w.Header().Set("ETag", fmt.Sprintf("%x", sha256.Sum256(contents))) + w.Header().Set("Content-Type", mimeType) + w.Write(contents) +} + func (h *Handle) Blob(w http.ResponseWriter, r *http.Request) { treePath := chi.URLParam(r, "*") ref := chi.URLParam(r, "ref") ref, _ = url.PathUnescape(ref) - l := h.l.With("handler", "FileContent", "ref", ref, "treePath", treePath) + l := h.l.With("handler", "Blob", "ref", ref, "treePath", treePath) path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) gr, err := git.Open(path, ref)