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.NewXrpcError( 43 xrpcerr.WithTag("RefNotFound"), 44 xrpcerr.WithMessage("repository or ref not found"), 45 ), http.StatusNotFound) 46 return 47 } 48 49 contents, err := gr.RawContent(treePath) 50 if err != nil { 51 x.Logger.Error("file content", "error", err.Error()) 52 writeError(w, xrpcerr.NewXrpcError( 53 xrpcerr.WithTag("FileNotFound"), 54 xrpcerr.WithMessage("file not found at the specified path"), 55 ), http.StatusNotFound) 56 return 57 } 58 59 mimeType := http.DetectContentType(contents) 60 61 if filepath.Ext(treePath) == ".svg" { 62 mimeType = "image/svg+xml" 63 } 64 65 if raw { 66 contentHash := sha256.Sum256(contents) 67 eTag := fmt.Sprintf("\"%x\"", contentHash) 68 69 switch { 70 case strings.HasPrefix(mimeType, "image/"), strings.HasPrefix(mimeType, "video/"): 71 if clientETag := r.Header.Get("If-None-Match"); clientETag == eTag { 72 w.WriteHeader(http.StatusNotModified) 73 return 74 } 75 w.Header().Set("ETag", eTag) 76 w.Header().Set("Content-Type", mimeType) 77 78 case strings.HasPrefix(mimeType, "text/"): 79 w.Header().Set("Cache-Control", "public, no-cache") 80 // serve all text content as text/plain 81 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 82 83 case isTextualMimeType(mimeType): 84 // handle textual application types (json, xml, etc.) as text/plain 85 w.Header().Set("Cache-Control", "public, no-cache") 86 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 87 88 default: 89 x.Logger.Error("attempted to serve disallowed file type", "mimetype", mimeType) 90 writeError(w, xrpcerr.NewXrpcError( 91 xrpcerr.WithTag("InvalidRequest"), 92 xrpcerr.WithMessage("only image, video, and text files can be accessed directly"), 93 ), http.StatusForbidden) 94 return 95 } 96 w.Write(contents) 97 return 98 } 99 100 isTextual := func(mt string) bool { 101 return strings.HasPrefix(mt, "text/") || isTextualMimeType(mt) 102 } 103 104 var content string 105 var encoding string 106 107 isBinary := !isTextual(mimeType) 108 109 if isBinary { 110 content = base64.StdEncoding.EncodeToString(contents) 111 encoding = "base64" 112 } else { 113 content = string(contents) 114 encoding = "utf-8" 115 } 116 117 response := tangled.RepoBlob_Output{ 118 Ref: ref, 119 Path: treePath, 120 Content: content, 121 Encoding: &encoding, 122 Size: &[]int64{int64(len(contents))}[0], 123 IsBinary: &isBinary, 124 } 125 126 if mimeType != "" { 127 response.MimeType = &mimeType 128 } 129 130 w.Header().Set("Content-Type", "application/json") 131 if err := json.NewEncoder(w).Encode(response); err != nil { 132 x.Logger.Error("failed to encode response", "error", err) 133 writeError(w, xrpcerr.NewXrpcError( 134 xrpcerr.WithTag("InternalServerError"), 135 xrpcerr.WithMessage("failed to encode response"), 136 ), http.StatusInternalServerError) 137 return 138 } 139} 140 141// isTextualMimeType returns true if the MIME type represents textual content 142// that should be served as text/plain for security reasons 143func isTextualMimeType(mimeType string) bool { 144 textualTypes := []string{ 145 "application/json", 146 "application/xml", 147 "application/yaml", 148 "application/x-yaml", 149 "application/toml", 150 "application/javascript", 151 "application/ecmascript", 152 } 153 154 return slices.Contains(textualTypes, mimeType) 155}