v0.32.0 53e25a3

raw.go RawHistoryFolder

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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 45

↑↓ moveenter openesc close