tinyrelayfile
tinyrelay/internal/daemon/client_browse_http.go/

internal/daemon/client_browse_http.go raw
1package daemon
2
3import (
4 "database/sql"
5 "errors"
6 "io"
7 "mime"
8 "net/http"
9 "os"
10 "path"
11 "strings"
12 "time"
13
14 "github.com/FelineStateMachine/tinyrelay/internal/blob"
15 gitrelay "github.com/FelineStateMachine/tinyrelay/tinygit"
16)
17
18func (t *Tenant) tryBrowseHTTP(w http.ResponseWriter, r *http.Request) bool {
19 if r.URL.Path != "/repo/raw" && r.URL.Path != "/files/raw" {
20 return false
21 }
22 ctx, finish := t.app.telemetry.Start(r.Context(), "browse-download")
23 r = r.WithContext(ctx)
24 outcome := "error"
25 defer func() { finish(outcome) }()
26 if r.Method != http.MethodGet && r.Method != http.MethodHead {
27 http.Error(w, "method not allowed", 405)
28 return true
29 }
30 actor, err := t.resolveUIActor(r)
31 if err == nil {
32 err = t.browseRead(r.Context(), actor)
33 }
34 if err != nil {
35 browseHTTPError(w, err)
36 return true
37 }
38 w.Header().Set("Cache-Control", "private, no-store")
39 w.Header().Set("Content-Security-Policy", "sandbox")
40 w.Header().Set("X-Content-Type-Options", "nosniff")
41 if r.URL.Path == "/repo/raw" {
42 err = t.sourceDownload(w, r, actor)
43 } else {
44 err = t.fileDownload(w, r, actor)
45 }
46 if err != nil {
47 browseHTTPError(w, err)
48 } else {
49 outcome = "success"
50 }
51 return true
52}
53
54func browseHTTPError(w http.ResponseWriter, err error) {
55 status := http.StatusBadRequest
56 switch {
57 case strings.HasPrefix(err.Error(), "conflict:"):
58 status = http.StatusConflict
59 case strings.HasPrefix(err.Error(), "auth-required:"):
60 status = http.StatusUnauthorized
61 case strings.HasPrefix(err.Error(), "restricted:"), strings.HasPrefix(err.Error(), "blocked:"):
62 status = http.StatusForbidden
63 case errors.Is(err, os.ErrNotExist), errors.Is(err, sql.ErrNoRows), strings.HasPrefix(err.Error(), "not found:"):
64 status = http.StatusNotFound
65 }
66 http.Error(w, err.Error(), status)
67}
68
69func (t *Tenant) sourceDownload(w http.ResponseWriter, r *http.Request, actor string) error {
70 if t.git == nil {
71 return os.ErrNotExist
72 }
73 values := r.URL.Query()
74 q := gitrelay.BrowseRequest{Owner: values.Get("owner"), Repo: values.Get("repo"), Ref: values.Get("ref"), Path: values.Get("path")}
75 repo, err := t.git.BrowseRepository(q.Owner, q.Repo)
76 if err != nil {
77 return err
78 }
79 if err := t.browseRepoAccess(r.Context(), actor, repo); err != nil {
80 return err
81 }
82 w.Header().Set("Content-Type", "application/octet-stream")
83 w.Header().Set("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": path.Base(q.Path)}))
84 if r.Method == http.MethodHead {
85 q.View = "file"
86 _, err = t.git.Browse(r.Context(), q)
87 return err
88 }
89 return t.git.WriteSource(r.Context(), q, w)
90}
91
92// mediaInline reports whether a stored type is safe to render in the browser.
93func mediaInline(typ string) bool {
94 return strings.HasPrefix(typ, "image/") || strings.HasPrefix(typ, "video/") || strings.HasPrefix(typ, "audio/") || typ == "application/pdf"
95}
96
97func (t *Tenant) fileDownload(w http.ResponseWriter, r *http.Request, actor string) error {
98 if !t.Policy().Features.Files {
99 return os.ErrNotExist
100 }
101 hash := r.URL.Query().Get("hash")
102 if err := t.fileReadAccess(r.Context(), actor, hash); err != nil {
103 return err
104 }
105 if err := t.roomAttachmentAccess(r.Context(), actor, hash); err != nil {
106 return err
107 }
108 entry, body, err := t.blobs.Get(r.Context(), hash)
109 if err != nil {
110 return err
111 }
112 defer body.Close()
113 // Recognised media is served inline under its own type so the browser can
114 // show it. Everything else downloads as an attachment.
115 typ := entry.Type
116 seeker, seekable := body.(io.ReadSeeker)
117 if typ == "application/octet-stream" && seekable {
118 head := make([]byte, 512)
119 n, _ := io.ReadFull(seeker, head)
120 typ = blob.DetectMediaType(head[:n])
121 if _, err := seeker.Seek(0, io.SeekStart); err != nil {
122 return err
123 }
124 }
125 if !mediaInline(typ) {
126 typ = "application/octet-stream"
127 }
128 w.Header().Set("Content-Type", typ)
129 if typ == "application/octet-stream" {
130 w.Header().Set("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": entry.SHA256}))
131 } else {
132 w.Header().Set("Content-Disposition", mime.FormatMediaType("inline", map[string]string{"filename": entry.SHA256}))
133 }
134 if seekable {
135 http.ServeContent(w, r, entry.SHA256, time.Unix(entry.Uploaded, 0), seeker)
136 return nil
137 }
138 if r.Method == http.MethodHead {
139 return nil
140 }
141 _, err = io.Copy(w, body)
142 return err
143}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs