tinyrelayfile
tinyrelay/internal/daemon/custom_views_http.go/

internal/daemon/custom_views_http.go raw
1package daemon
2
3// GET /views/<name>/<hash>[.<svg|png>] serves a custom view artifact under a
4// sandbox policy. Reads require both the view's audience and a visible source.
5
6import (
7 "crypto/sha256"
8 "encoding/hex"
9 "net/http"
10 "strconv"
11 "strings"
12
13 "github.com/FelineStateMachine/tinyrelay/internal/relay"
14 "github.com/FelineStateMachine/tinyrelay/internal/views"
15)
16
17func isViewArtifactPath(path string) bool {
18 return strings.HasPrefix(path, "/views/")
19}
20
21func (t *customViewService) viewArtifactHTTP(w http.ResponseWriter, r *http.Request) {
22 ctx, finish := t.telemetry.Start(r.Context(), "view")
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", http.StatusMethodNotAllowed)
28 outcome = "invalid"
29 return
30 }
31 parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/views/"), "/")
32 if len(parts) != 2 {
33 http.NotFound(w, r)
34 outcome = "invalid"
35 return
36 }
37 name := parts[0]
38 hash, extension, hasExtension := strings.Cut(parts[1], ".")
39 if !views.NamePattern.MatchString(name) || !views.HashPattern.MatchString(hash) || (hasExtension && extension != "svg" && extension != "png") {
40 http.NotFound(w, r)
41 outcome = "invalid"
42 return
43 }
44 actor, err := t.resolveActor(r)
45 if err != nil {
46 http.Error(w, err.Error(), http.StatusUnauthorized)
47 outcome = "unauthorized"
48 return
49 }
50 record, err := t.customViewArtifact(ctx, name, hash)
51 if err != nil {
52 if strings.HasPrefix(err.Error(), "not found:") {
53 http.NotFound(w, r)
54 outcome = "invalid"
55 return
56 }
57 http.Error(w, err.Error(), http.StatusInternalServerError)
58 return
59 }
60 wantType := map[string]string{"svg": "image/svg+xml", "png": "image/png"}[extension]
61 if extension != "" && record.Type != wantType {
62 http.NotFound(w, r)
63 outcome = "invalid"
64 return
65 }
66 if record.Audience == "members" {
67 role, err := t.community.Role(ctx, actor)
68 if err != nil {
69 http.Error(w, err.Error(), http.StatusInternalServerError)
70 return
71 }
72 if actor == "" {
73 http.Error(w, "auth-required: this view is for members", http.StatusUnauthorized)
74 outcome = "unauthorized"
75 return
76 }
77 if role != "member" && role != "moderator" && role != "owner" {
78 http.Error(w, "restricted: this view is for members", http.StatusForbidden)
79 outcome = "unauthorized"
80 return
81 }
82 } else if err := t.browseRead(ctx, actor); err != nil {
83 browseHTTPError(w, err)
84 outcome = "unauthorized"
85 return
86 }
87 if !t.artifactHTTPSourceVisible(ctx, name, hash, relay.Session{PubKeys: []string{actor}, RelayURL: t.publicURL}) {
88 http.NotFound(w, r)
89 outcome = "invalid"
90 return
91 }
92 body, err := record.body()
93 if err != nil {
94 http.Error(w, "artifact is unreadable", http.StatusInternalServerError)
95 return
96 }
97 w.Header().Set("Content-Type", record.Type)
98 w.Header().Set("Content-Length", strconv.Itoa(len(body)))
99 w.Header().Set("Content-Security-Policy", "sandbox; default-src 'none'; style-src 'unsafe-inline'")
100 w.Header().Set("X-Content-Type-Options", "nosniff")
101 etagBytes := sha256.Sum256(body)
102 etag := `"` + hex.EncodeToString(etagBytes[:]) + `"`
103 w.Header().Set("ETag", etag)
104 w.Header().Set("Cache-Control", "private, no-cache")
105 outcome = "ok"
106 if r.Header.Get("If-None-Match") == etag {
107 w.WriteHeader(http.StatusNotModified)
108 return
109 }
110 if r.Method == http.MethodHead {
111 w.WriteHeader(http.StatusOK)
112 return
113 }
114 _, _ = w.Write(body)
115}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

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

Refs