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