This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knotmirror / xrpc / git_get_blob.go
4.1 kB 137 lines
1package xrpc 2 3import ( 4 "context" 5 "crypto/sha256" 6 "fmt" 7 "io" 8 "net/http" 9 "path/filepath" 10 "slices" 11 "strings" 12 13 "github.com/bluesky-social/indigo/atproto/atclient" 14 "github.com/bluesky-social/indigo/atproto/syntax" 15 "github.com/go-git/go-git/v5/plumbing/object" 16 "tangled.org/core/knotserver/git" 17) 18 19func (x *Xrpc) GetBlob(w http.ResponseWriter, r *http.Request) { 20 var ( 21 repoQuery = r.URL.Query().Get("repo") 22 ref = r.URL.Query().Get("ref") // ref can be empty (git.Open handles this) 23 path = r.URL.Query().Get("path") 24 ) 25 26 repo, err := syntax.ParseATURI(repoQuery) 27 if err != nil || repo.RecordKey() == "" { 28 writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) 29 return 30 } 31 32 l := x.logger.With("repo", repo, "ref", ref, "path", path) 33 34 if path == "" { 35 writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "missing path parameter"}) 36 return 37 } 38 39 file, err := x.getFile(r.Context(), repo, ref, path) 40 if err != nil { 41 l.Warn("local mirror failed, trying proxy", "err", err) 42 if x.proxyToKnot(w, r, repo) { 43 return 44 } 45 writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to get blob"}) 46 return 47 } 48 49 reader, err := file.Reader() 50 if err != nil { 51 l.Error("failed to read blob", "err", err) 52 writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to read the blob"}) 53 return 54 } 55 defer reader.Close() 56 57 // default to octet-stream for large blobs 58 if file.Size > 1000*1000 { // 1MB 59 w.Header().Set("Content-Type", "application/octet-stream") 60 if _, err := io.Copy(w, reader); err != nil { 61 l.Error("failed to serve the blob", "err", err) 62 } 63 return 64 } 65 66 contents, err := io.ReadAll(reader) 67 if err != nil { 68 l.Error("failed to read blob content", "err", err) 69 writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to read the blob"}) 70 return 71 } 72 73 mimeType := http.DetectContentType(contents) 74 // override MIME types for formats that http.DetectContentType does not recognize 75 switch filepath.Ext(path) { 76 case ".svg": 77 mimeType = "image/svg+xml" 78 case ".avif": 79 mimeType = "image/avif" 80 case ".jxl": 81 mimeType = "image/jxl" 82 case ".heic", ".heif": 83 mimeType = "image/heif" 84 } 85 86 switch { 87 case strings.HasPrefix(mimeType, "image/"), strings.HasPrefix(mimeType, "video/"): 88 eTag := fmt.Sprintf("\"%x\"", sha256.Sum256(contents)) 89 if clientETag := r.Header.Get("If-None-Match"); clientETag == eTag { 90 w.WriteHeader(http.StatusNotModified) 91 return 92 } 93 w.Header().Set("ETag", eTag) 94 w.Header().Set("Content-Type", mimeType) 95 96 case strings.HasPrefix(mimeType, "text/") || isTextualMimeType(mimeType): 97 w.Header().Set("Cache-Control", "public, no-cache") 98 // serve all text content as text/plain 99 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 100 101 default: 102 l.Error("attempted to serve disallowed file type", "mimetype", mimeType) 103 writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InvalidRequest", Message: "only image, video, and text files can be accessed directly"}) 104 return 105 } 106 w.Write(contents) 107} 108 109func (x *Xrpc) getFile(ctx context.Context, repo syntax.ATURI, ref, path string) (*object.File, error) { 110 repoPath, err := x.makeRepoPath(ctx, repo) 111 if err != nil { 112 return nil, fmt.Errorf("resolving repo at-uri: %w", err) 113 } 114 115 gr, err := git.Open(repoPath, ref) 116 if err != nil { 117 return nil, fmt.Errorf("opening git repo: %w", err) 118 } 119 120 return gr.File(path) 121} 122 123var textualMimeTypes = []string{ 124 "application/json", 125 "application/xml", 126 "application/yaml", 127 "application/x-yaml", 128 "application/toml", 129 "application/javascript", 130 "application/ecmascript", 131} 132 133// isTextualMimeType returns true if the MIME type represents textual content 134// that should be served as text/plain for security reasons 135func isTextualMimeType(mimeType string) bool { 136 return slices.Contains(textualMimeTypes, mimeType) 137}