File view with blame information shown in the left gutter beside each line.

barerepo / server / internal/httpd/server.go
296 lines · 9.4kb · 133728eaa05486504991b230f9d4c7e986b7defc
log files threads runs releases config jump to file t
133728e barerepo 1mo
1
package httpd
133728e barerepo 1mo
2
133728e barerepo 1mo
3
import (
133728e barerepo 1mo
4
"context"
133728e barerepo 1mo
5
"crypto/sha256"
133728e barerepo 1mo
6
"embed"
133728e barerepo 1mo
7
"encoding/hex"
133728e barerepo 1mo
8
"errors"
133728e barerepo 1mo
9
"io/fs"
133728e barerepo 1mo
10
"log/slog"
133728e barerepo 1mo
11
"net/http"
133728e barerepo 1mo
12
"sort"
133728e barerepo 1mo
13
"strings"
133728e barerepo 1mo
14
"sync"
133728e barerepo 1mo
15
"time"
133728e barerepo 1mo
16
133728e barerepo 1mo
17
"github.com/barerepo/server/internal/artifact"
133728e barerepo 1mo
18
"github.com/barerepo/server/internal/config"
133728e barerepo 1mo
19
"github.com/barerepo/server/internal/gitx"
133728e barerepo 1mo
20
"github.com/barerepo/server/internal/proposal"
133728e barerepo 1mo
21
"github.com/barerepo/server/internal/repo"
133728e barerepo 1mo
22
"github.com/barerepo/server/internal/repocfg"
133728e barerepo 1mo
23
"github.com/barerepo/server/internal/store"
133728e barerepo 1mo
24
"github.com/barerepo/server/internal/token"
133728e barerepo 1mo
25
"github.com/barerepo/server/internal/transport"
133728e barerepo 1mo
26
)
133728e barerepo 1mo
27
133728e barerepo 1mo
28
//go:embed static
133728e barerepo 1mo
29
var static embed.FS
133728e barerepo 1mo
30
133728e barerepo 1mo
31
type Server struct {
133728e barerepo 1mo
32
Cfg config.Config
133728e barerepo 1mo
33
DB *store.DB
133728e barerepo 1mo
34
Transport *transport.Server
133728e barerepo 1mo
35
Log *slog.Logger
133728e barerepo 1mo
36
133728e barerepo 1mo
37
// signups counts per address for chapter 27, and losing it on restart is not worth a table.
133728e barerepo 1mo
38
signupsMu sync.Mutex
133728e barerepo 1mo
39
signups map[string][]time.Time
133728e barerepo 1mo
40
133728e barerepo 1mo
41
// comments counts per account per thread, the other half of chapter 27, on the same terms.
133728e barerepo 1mo
42
commentsMu sync.Mutex
133728e barerepo 1mo
43
comments map[string][]time.Time
133728e barerepo 1mo
44
}
133728e barerepo 1mo
45
133728e barerepo 1mo
46
// assetTag is a hash of everything under static, so a changed stylesheet gets a url nobody has cached.
133728e barerepo 1mo
47
var assetTag = hashAssets()
133728e barerepo 1mo
48
133728e barerepo 1mo
49
// hashAssets reads the embedded files once, in name order, because a map's order is not one.
133728e barerepo 1mo
50
func hashAssets() string {
133728e barerepo 1mo
51
names, err := fs.Glob(static, "static/*")
133728e barerepo 1mo
52
if err != nil {
133728e barerepo 1mo
53
return Version
133728e barerepo 1mo
54
}
133728e barerepo 1mo
55
sort.Strings(names)
133728e barerepo 1mo
56
sum := sha256.New()
133728e barerepo 1mo
57
for _, name := range names {
133728e barerepo 1mo
58
body, err := static.ReadFile(name)
133728e barerepo 1mo
59
if err != nil {
133728e barerepo 1mo
60
continue
133728e barerepo 1mo
61
}
133728e barerepo 1mo
62
sum.Write([]byte(name))
133728e barerepo 1mo
63
sum.Write(body)
133728e barerepo 1mo
64
}
133728e barerepo 1mo
65
return hex.EncodeToString(sum.Sum(nil))[:12]
133728e barerepo 1mo
66
}
133728e barerepo 1mo
67
133728e barerepo 1mo
68
// keepable lets a browser keep the stylesheet, because a page that refetches it flashes unstyled.
133728e barerepo 1mo
69
func keepable(next http.Handler) http.Handler {
133728e barerepo 1mo
70
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
133728e barerepo 1mo
71
if r.URL.Query().Get("v") == assetTag {
133728e barerepo 1mo
72
// The url carries the hash of the body, so what is under it can never change. Chapter 25.
133728e barerepo 1mo
73
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
133728e barerepo 1mo
74
} else {
133728e barerepo 1mo
75
// An unversioned url is somebody's bookmark, and a minute is enough to stop a flash.
133728e barerepo 1mo
76
w.Header().Set("Cache-Control", "public, max-age=60")
133728e barerepo 1mo
77
}
133728e barerepo 1mo
78
next.ServeHTTP(w, r)
133728e barerepo 1mo
79
})
133728e barerepo 1mo
80
}
133728e barerepo 1mo
81
133728e barerepo 1mo
82
// Handler is the whole http surface. Routes are in appendix C.
133728e barerepo 1mo
83
func (s *Server) Handler() http.Handler {
133728e barerepo 1mo
84
// The ring is built once here, so a restart still knows whose keys signed what.
133728e barerepo 1mo
85
s.rebuildKeyring(context.Background())
133728e barerepo 1mo
86
133728e barerepo 1mo
87
mux := http.NewServeMux()
133728e barerepo 1mo
88
133728e barerepo 1mo
89
sub, err := fs.Sub(static, "static")
133728e barerepo 1mo
90
if err != nil {
133728e barerepo 1mo
91
panic(err)
133728e barerepo 1mo
92
}
133728e barerepo 1mo
93
mux.Handle("GET /static/", webPolicy(keepable(http.StripPrefix("/static/", http.FileServerFS(sub)))))
133728e barerepo 1mo
94
133728e barerepo 1mo
95
// Matched by shape, not prefix, because the same prefix carries every web route too.
133728e barerepo 1mo
96
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
133728e barerepo 1mo
97
if route, ok := parseGitPath(r.URL.Path); ok {
133728e barerepo 1mo
98
switch {
133728e barerepo 1mo
99
case r.Method == http.MethodGet && route.Advertise,
133728e barerepo 1mo
100
r.Method == http.MethodPost && route.Service != "":
133728e barerepo 1mo
101
s.serveGit(w, r, route)
133728e barerepo 1mo
102
return
133728e barerepo 1mo
103
}
133728e barerepo 1mo
104
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
133728e barerepo 1mo
105
return
133728e barerepo 1mo
106
}
133728e barerepo 1mo
107
webPolicy(http.HandlerFunc(s.serveWeb)).ServeHTTP(w, r)
133728e barerepo 1mo
108
})
133728e barerepo 1mo
109
return baseHeaders(mux)
133728e barerepo 1mo
110
}
133728e barerepo 1mo
111
133728e barerepo 1mo
112
// baseHeaders go on everything, including the git routes.
133728e barerepo 1mo
113
func baseHeaders(next http.Handler) http.Handler {
133728e barerepo 1mo
114
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
133728e barerepo 1mo
115
w.Header().Set("X-Content-Type-Options", "nosniff")
133728e barerepo 1mo
116
w.Header().Set("Referrer-Policy", "no-referrer")
133728e barerepo 1mo
117
next.ServeHTTP(w, r)
133728e barerepo 1mo
118
})
133728e barerepo 1mo
119
}
133728e barerepo 1mo
120
133728e barerepo 1mo
121
// webPolicy wraps the handlers, not the path, because a repository named `git-anything` is legal.
133728e barerepo 1mo
122
func webPolicy(next http.Handler) http.Handler {
133728e barerepo 1mo
123
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
133728e barerepo 1mo
124
// 'self' is for the two shortcuts only, and inline, eval and other hosts stay blocked.
133728e barerepo 1mo
125
w.Header().Set("Content-Security-Policy",
133728e barerepo 1mo
126
"default-src 'none'; img-src 'self'; style-src 'self'; script-src 'self'; font-src 'self'; "+
133728e barerepo 1mo
127
"form-action 'self'; frame-ancestors 'none'; base-uri 'none'")
133728e barerepo 1mo
128
next.ServeHTTP(w, r)
133728e barerepo 1mo
129
})
133728e barerepo 1mo
130
}
133728e barerepo 1mo
131
133728e barerepo 1mo
132
// authenticate reads a token from the basic password field, no credential is anonymous, and the scope rides along because chapter 15's job token names one repository and no more.
133728e barerepo 1mo
133
func (s *Server) authenticate(ctx context.Context, r *http.Request) (account, scope string, err error) {
133728e barerepo 1mo
134
_, pass, ok := r.BasicAuth()
133728e barerepo 1mo
135
if !ok || pass == "" {
133728e barerepo 1mo
136
return "", "", nil
133728e barerepo 1mo
137
}
133728e barerepo 1mo
138
t, err := s.DB.AccountForToken(ctx, token.Git, pass)
133728e barerepo 1mo
139
if err != nil {
133728e barerepo 1mo
140
return "", "", errors.New("that token is not valid")
133728e barerepo 1mo
141
}
133728e barerepo 1mo
142
return t.Account, t.Scope, nil
133728e barerepo 1mo
143
}
133728e barerepo 1mo
144
133728e barerepo 1mo
145
// askForCredentials is the one 401, and a private repository gets it, because existence leaks.
133728e barerepo 1mo
146
func (s *Server) askForCredentials(w http.ResponseWriter) {
133728e barerepo 1mo
147
w.Header().Set("WWW-Authenticate", `Basic realm="barerepo"`)
133728e barerepo 1mo
148
http.Error(w, "a token goes in the password field. the username is ignored.", http.StatusUnauthorized)
133728e barerepo 1mo
149
}
133728e barerepo 1mo
150
133728e barerepo 1mo
151
// movedPath rewrites the path for a moved repository, keeping whatever git appended.
133728e barerepo 1mo
152
func (s *Server) movedPath(r *http.Request, to transport.Redirect) string {
133728e barerepo 1mo
153
parts := strings.SplitN(strings.TrimPrefix(r.URL.Path, "/"), "/", 3)
133728e barerepo 1mo
154
rest := ""
133728e barerepo 1mo
155
if len(parts) == 3 {
133728e barerepo 1mo
156
rest = "/" + parts[2]
133728e barerepo 1mo
157
}
133728e barerepo 1mo
158
u := "/" + to.Owner + "/" + to.Name + rest
133728e barerepo 1mo
159
if r.URL.RawQuery != "" {
133728e barerepo 1mo
160
u += "?" + r.URL.RawQuery
133728e barerepo 1mo
161
}
133728e barerepo 1mo
162
return u
133728e barerepo 1mo
163
}
133728e barerepo 1mo
164
133728e barerepo 1mo
165
func (s *Server) oops(w http.ResponseWriter, r *http.Request, err error) {
133728e barerepo 1mo
166
s.log(r, err)
133728e barerepo 1mo
167
http.Error(w, "something went wrong on the server", http.StatusInternalServerError)
133728e barerepo 1mo
168
}
133728e barerepo 1mo
169
133728e barerepo 1mo
170
func (s *Server) log(r *http.Request, err error) {
133728e barerepo 1mo
171
if s.Log == nil {
133728e barerepo 1mo
172
return
133728e barerepo 1mo
173
}
133728e barerepo 1mo
174
s.Log.Error("request failed", "method", r.Method, "path", r.URL.Path, "err", err)
133728e barerepo 1mo
175
}
133728e barerepo 1mo
176
133728e barerepo 1mo
177
// TrashWindow is chapter 44.4's 30 days, and the config page states the number.
133728e barerepo 1mo
178
const TrashWindow = 30 * 24 * time.Hour
133728e barerepo 1mo
179
133728e barerepo 1mo
180
// Sweep discards what has expired, and a failure is logged for the next pass to retry.
133728e barerepo 1mo
181
func (s *Server) Sweep(ctx context.Context) {
133728e barerepo 1mo
182
if err := s.DB.SweepExpired(ctx); err != nil {
133728e barerepo 1mo
183
s.Log.Error("sweep: expired tokens", "err", err)
133728e barerepo 1mo
184
}
133728e barerepo 1mo
185
if err := s.DB.DropOldEvents(ctx); err != nil {
133728e barerepo 1mo
186
s.Log.Error("sweep: old events", "err", err)
133728e barerepo 1mo
187
}
133728e barerepo 1mo
188
if n, err := s.DB.RequeueLostJobs(ctx, 30*time.Minute); err != nil {
133728e barerepo 1mo
189
s.Log.Error("sweep: lost jobs", "err", err)
133728e barerepo 1mo
190
} else if n > 0 {
133728e barerepo 1mo
191
s.Log.Info("requeued jobs whose runner stopped", "count", n)
133728e barerepo 1mo
192
}
133728e barerepo 1mo
193
if n := gitx.CloseIdleReaders(gitx.IdleLife); n > 0 {
133728e barerepo 1mo
194
s.Log.Info("closed idle object readers", "count", n)
133728e barerepo 1mo
195
}
133728e barerepo 1mo
196
if n, err := repo.EmptyTrash(s.Cfg.Paths.Repos, TrashWindow, time.Now()); err != nil {
133728e barerepo 1mo
197
s.Log.Error("sweep: trash", "err", err)
133728e barerepo 1mo
198
} else if n > 0 {
133728e barerepo 1mo
199
s.Log.Info("erased trash past its window", "count", n)
133728e barerepo 1mo
200
}
133728e barerepo 1mo
201
s.sweepRateBuckets()
133728e barerepo 1mo
202
if n, err := artifact.SweepParts(s.Cfg.Paths.Artifacts, artifact.PartWindow, time.Now()); err != nil {
133728e barerepo 1mo
203
s.Log.Error("sweep: unfinished uploads", "err", err)
133728e barerepo 1mo
204
} else if n > 0 {
133728e barerepo 1mo
205
s.Log.Info("removed uploads no process finished", "count", n)
133728e barerepo 1mo
206
}
133728e barerepo 1mo
207
s.expireProposals(ctx)
133728e barerepo 1mo
208
}
133728e barerepo 1mo
209
133728e barerepo 1mo
210
// sweepRateBuckets drops the counters nobody is spending, since a key per thread is a key forever.
133728e barerepo 1mo
211
func (s *Server) sweepRateBuckets() {
133728e barerepo 1mo
212
cutoff := time.Now().Add(-time.Hour)
133728e barerepo 1mo
213
prune := func(mu *sync.Mutex, buckets map[string][]time.Time) {
133728e barerepo 1mo
214
mu.Lock()
214 of 296 lines · the whole file
history · rawbarerepo 0.1.0