this repo has no description
1package xrpc 2 3import ( 4 "crypto/sha256" 5 "encoding/base64" 6 "encoding/json" 7 "fmt" 8 "net/http" 9 "path/filepath" 10 "slices" 11 "strings" 12 13 "tangled.sh/tangled.sh/core/api/tangled" 14 "tangled.sh/tangled.sh/core/knotserver/git" 15 xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors" 16) 17 18func (x *Xrpc) RepoBlob(w http.ResponseWriter, r *http.Request) { 19 repo := r.URL.Query().Get("repo") 20 repoPath, err := x.parseRepoParam(repo) 21 if err != nil { 22 writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest) 23 return 24 } 25 26 ref := r.URL.Query().Get("ref") 27 // ref can be empty (git.Open handles this) 28 29 treePath := r.URL.Query().Get("path") 30 if treePath == "" { 31 writeError(w, xrpcerr.NewXrpcError( 32 xrpcerr.WithTag("InvalidRequest"), 33 xrpcerr.WithMessage("missing path parameter"), 34 ), http.StatusBadRequest) 35 return 36 } 37 38 raw := r.URL.Query().Get("raw") == "true" 39 40 gr, err := git.Open(repoPath, ref) 41 if err != nil { 42 writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound) 43 return 44 } 45 46 contents, err := gr.RawContent(treePath) 47 if err != nil { 48 x.Logger.Error("file content", "error", err.Error()) 49 writeError(w, xrpcerr.NewXrpcError( 50 xrpcerr.WithTag("FileNotFound"), 51 xrpcerr.WithMessage("file not found at the specified path"), 52 ), http.StatusNotFound) 53 return 54 } 55 56 mimeType := http.DetectContentType(contents) 57 58 if filepath.Ext(treePath) == ".svg" { 59 mimeType = "image/svg+xml" 60 } 61 62 if raw { 63 contentHash := sha256.Sum256(contents) 64 eTag := fmt.Sprintf("\"%x\"", contentHash) 65 66 switch { 67 case strings.HasPrefix(mimeType, "image/"), strings.HasPrefix(mimeType, "video/"): 68 if clientETag := r.Header.Get("If-None-Match"); clientETag == eTag { 69 w.WriteHeader(http.StatusNotModified) 70 return 71 } 72 w.Header().Set("ETag", eTag) 73 w.Header().Set("Content-Type", mimeType) 74 75 case strings.HasPrefix(mimeType, "text/"): 76 w.Header().Set("Cache-Control", "public, no-cache") 77 // serve all text content as text/plain 78 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 79 80 case isTextualMimeType(mimeType): 81 // handle textual application types (json, xml, etc.) as text/plain 82 w.Header().Set("Cache-Control", "public, no-cache") 83 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 84 85 default: 86 x.Logger.Error("attempted to serve disallowed file type", "mimetype", mimeType) 87 writeError(w, xrpcerr.NewXrpcError( 88 xrpcerr.WithTag("InvalidRequest"), 89 xrpcerr.WithMessage("only image, video, and text files can be accessed directly"), 90 ), http.StatusForbidden) 91 return 92 } 93 w.Write(contents) 94 return 95 } 96 97 isTextual := func(mt string) bool { 98 return strings.HasPrefix(mt, "text/") || isTextualMimeType(mt) 99 } 100 101 var content string 102 var encoding string 103 104 isBinary := !isTextual(mimeType) 105 106 if isBinary { 107 content = base64.StdEncoding.EncodeToString(contents) 108 encoding = "base64" 109 } else { 110 content = string(contents) 111 encoding = "utf-8" 112 } 113 114 response := tangled.RepoBlob_Output{ 115 Ref: ref, 116 Path: treePath, 117 Content: content, 118 Encoding: &encoding, 119 Size: &[]int64{int64(len(contents))}[0], 120 IsBinary: &isBinary, 121 } 122 123 if mimeType != "" { 124 response.MimeType = &mimeType 125 } 126 127 w.Header().Set("Content-Type", "application/json") 128 if err := json.NewEncoder(w).Encode(response); err != nil { 129 x.Logger.Error("failed to encode response", "error", err) 130 writeError(w, xrpcerr.NewXrpcError( 131 xrpcerr.WithTag("InternalServerError"), 132 xrpcerr.WithMessage("failed to encode response"), 133 ), http.StatusInternalServerError) 134 return 135 } 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 textualTypes := []string{ 142 "application/json", 143 "application/xml", 144 "application/yaml", 145 "application/x-yaml", 146 "application/toml", 147 "application/javascript", 148 "application/ecmascript", 149 } 150 151 return slices.Contains(textualTypes, mimeType) 152}