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}