forked from
tangled.org/core
Monorepo for Tangled — https://tangled.org
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}