forked from tangled.org/core
this repo has no description
at master 3.6 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 contents, err := gr.RawContent(treePath) 46 if err != nil { 47 x.Logger.Error("file content", "error", err.Error(), "treePath", treePath) 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 w.Header().Set("Content-Type", mimeType) 73 74 case strings.HasPrefix(mimeType, "text/"): 75 w.Header().Set("Cache-Control", "public, no-cache") 76 // serve all text content as text/plain 77 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 78 79 case isTextualMimeType(mimeType): 80 // handle textual application types (json, xml, etc.) as text/plain 81 w.Header().Set("Cache-Control", "public, no-cache") 82 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 83 84 default: 85 x.Logger.Error("attempted to serve disallowed file type", "mimetype", mimeType) 86 writeError(w, xrpcerr.NewXrpcError( 87 xrpcerr.WithTag("InvalidRequest"), 88 xrpcerr.WithMessage("only image, video, and text files can be accessed directly"), 89 ), http.StatusForbidden) 90 return 91 } 92 w.Write(contents) 93 return 94 } 95 96 isTextual := func(mt string) bool { 97 return strings.HasPrefix(mt, "text/") || isTextualMimeType(mt) 98 } 99 100 var content string 101 var encoding string 102 103 isBinary := !isTextual(mimeType) 104 105 if isBinary { 106 content = base64.StdEncoding.EncodeToString(contents) 107 encoding = "base64" 108 } else { 109 content = string(contents) 110 encoding = "utf-8" 111 } 112 113 response := tangled.RepoBlob_Output{ 114 Ref: ref, 115 Path: treePath, 116 Content: content, 117 Encoding: &encoding, 118 Size: &[]int64{int64(len(contents))}[0], 119 IsBinary: &isBinary, 120 } 121 122 if mimeType != "" { 123 response.MimeType = &mimeType 124 } 125 126 writeJson(w, response) 127} 128 129// isTextualMimeType returns true if the MIME type represents textual content 130// that should be served as text/plain for security reasons 131func isTextualMimeType(mimeType string) bool { 132 textualTypes := []string{ 133 "application/json", 134 "application/xml", 135 "application/yaml", 136 "application/x-yaml", 137 "application/toml", 138 "application/javascript", 139 "application/ecmascript", 140 } 141 142 return slices.Contains(textualTypes, mimeType) 143}