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