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}