Monorepo for Tangled
0

Configure Feed

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

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