1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package web
4
5import (
6 "context"
7 "mime"
8 "net/http"
9 "path"
10 "strconv"
11 "time"
12
13 "github.com/bnema/zerowrap"
14)
15
16// rawTimeout bounds the stream of one file to the visitor.
17const rawTimeout = 5 * time.Minute
18
19// rawCSP makes a raw file inert if a browser opens it as a document: it can
20// load nothing and run nothing, whatever it contains.
21const rawCSP = "default-src 'none'; sandbox"
22
23// raw streams the content of a file. Text is served as text/plain, the four
24// image types (by their bytes, never SVG) as images, and anything else as an
25// attachment of unknown type. A file is never served as HTML or SVG, so what
26// a repository holds cannot run in the origin of the forge.
27func (s *Server) raw(w http.ResponseWriter, r *http.Request) {
28 ctx, cancel := context.WithTimeout(r.Context(), rawTimeout)
29 defer cancel()
30 b, t, repo, err := s.rawOpen(ctx, r)
31 if err != nil {
32 s.fail(ctx, w, r, err)
33 return
34 }
35 defer b.close()
36
37 // The type needs the first bytes; headers must not be sent before.
38 if err := b.fill(512); err != nil {
39 s.fail(ctx, w, r, err)
40 return
41 }
42 typ, attach := rawType(b)
43 h := w.Header()
44 h.Set("Content-Type", typ)
45 h.Set("X-Content-Type-Options", "nosniff")
46 h.Set("Content-Security-Policy", rawCSP)
47 h.Set("Content-Length", strconv.FormatInt(b.info.GetSize(), 10))
48 if attach {
49 name := path.Base(t.path)
50 cd := mime.FormatMediaType("attachment", map[string]string{"filename": name})
51 if cd == "" {
52 cd = "attachment"
53 }
54 h.Set("Content-Disposition", cd)
55 }
56 if etag := b.info.GetId(); etag != "" {
57 h.Set("ETag", `"`+etag+`"`)
58 }
59 // Revalidated every time, even at a full commit id: a repository can turn
60 // private, and a shared cache must not keep serving its files (AC2). The
61 // ETag keeps the revalidation cheap.
62 cacheHeaders(ctx, h, "no-cache", sessionCache)
63 if etag := h.Get("ETag"); etag != "" && etagMatches(r.Header.Get("If-None-Match"), etag) {
64 w.WriteHeader(http.StatusNotModified)
65 return
66 }
67 w.WriteHeader(http.StatusOK)
68 if r.Method == http.MethodHead {
69 return
70 }
71 if _, err := w.Write(b.buf); err != nil {
72 return
73 }
74 b.buf = nil
75 for b.stream.Receive() {
76 if _, err := w.Write(b.stream.Msg().GetChunk()); err != nil {
77 return
78 }
79 }
80 if err := b.stream.Err(); err != nil {
81 // Headers are gone: all that is left is to cut the answer short, so
82 // that the client sees it is incomplete.
83 log := zerowrap.FromCtx(ctx)
84 log.Error().Err(err).Str("repo", repo).Str("path", r.URL.Path).Msg("raw stream failed")
85 panic(http.ErrAbortHandler)
86 }
87}
88
89// rawOpen finds the file a raw URL designates and opens its stream.
90func (s *Server) rawOpen(ctx context.Context, r *http.Request) (*blobStream, target, string, error) {
91 d, err := s.loadRepo(ctx, r.PathValue("repo"))
92 if err != nil {
93 return nil, target{}, "", err
94 }
95 t, ok := d.locate(r.PathValue("rest"))
96 if !ok {
97 return nil, target{}, "", errNotFound
98 }
99 b, err := s.openBlob(ctx, d.name(), t)
100 if err != nil {
101 return nil, target{}, "", err
102 }
103 return b, t, d.name(), nil
104}
105
106// rawType is the Content-Type of a file and whether it is an attachment.
107// b.buf holds the first bytes (up to 512 when the file has that many).
108func rawType(b *blobStream) (typ string, attachment bool) {
109 if img := sniffImage(b.buf); img != "" {
110 return img, false
111 }
112 if !b.info.GetBinary() {
113 return "text/plain; charset=utf-8", false
114 }
115 return "application/octet-stream", true
116}