internal/httpd/control.go
306 lines · 9582 bytes
20 symbols in this file
Server.runControlServer.runControlCodeServer.runControlStreamServer.doneServer.runControlStdinServer.runControlStdinCodeServer.runControlReaderServer.runControlIntoServer.runControlIntoCodeServer.dispatchIntoServer.dispatchIntoStdinServer.dispatchJSONauthorNamesServer.authorNamesauthorNames.nameauthorNames.accountnamedCommitServer.namedCommitsServer.namedTipServer.webViewer
1package httpd
2
3import (
4 "bytes"
5 "encoding/json"
6 "io"
7 "net/http"
8 "strings"
9
10 "gitbay.org/gitbay/internal/control"
11 "gitbay.org/gitbay/internal/gitutil"
12 "gitbay.org/gitbay/internal/protocol"
13 "gitbay.org/gitbay/internal/store"
14)
15
16// runControl executes a control command as the browser session's user,
17// through the same registry the CLI and the JSON API reach. Web writes
18// never reimplement command logic — merge gates, review rules, and audit
19// entries stay in one place — so the surfaces cannot drift apart.
20//
21// ViaAPI is set, which marks the request as one that arrived over HTTP.
22// Nothing is held back from that door any more (#234): what a caller may
23// do is the account's rights and its credential's scope, decided in one
24// place for every surface.
25func (s *Server) runControl(u store.User, argv []string) (out string, msg string, ok bool) {
26 out, msg, code := s.runControlCode(u, argv)
27 return out, msg, code == protocol.ExitOK
28}
29
30// runControlCode is runControl with the exit code, for handlers that
31// answer a form: not-found and denied deserve their own statuses rather
32// than a redirect carrying the message (#106).
33func (s *Server) runControlCode(u store.User, argv []string) (out string, msg string, code int) {
34 var stdout, stderr bytes.Buffer
35 ctx := &control.Ctx{
36 User: u,
37 Source: control.SourceWeb,
38 Scope: "full",
39 Store: s.st,
40 Cfg: s.cfg,
41 Stdin: strings.NewReader(""),
42 Stdout: &stdout,
43 Stderr: &stderr,
44 ViaAPI: true,
45 }
46 code = control.Dispatch(ctx, argv)
47 m := strings.TrimSpace(stderr.String())
48 if m == "" {
49 m = strings.TrimSpace(stdout.String())
50 }
51 return stdout.String(), m, code
52}
53
54// runControlStream runs a command whose output is written as it is
55// produced: stdout goes to out, and done ends the command when the
56// request does. msg is stderr.
57func (s *Server) runControlStream(u store.User, argv []string, out io.Writer, done <-chan struct{}) (msg string, code int) {
58 var stderr bytes.Buffer
59 ctx := &control.Ctx{
60 User: u,
61 Source: control.SourceWeb,
62 Scope: "full",
63 Store: s.st,
64 Cfg: s.cfg,
65 Stdin: strings.NewReader(""),
66 Stdout: out,
67 Stderr: &stderr,
68 ViaAPI: true,
69 Done: done,
70 Stopping: s.stopping,
71 }
72 code = control.Dispatch(ctx, argv)
73 return strings.TrimSpace(stderr.String()), code
74}
75
76// done finishes a form action by exit code: back to the page on success,
77// the 404 page when the thing does not exist, and back to the page with
78// the message for anything else. A refusal is feedback on the page a
79// person was looking at, whether it is a merge gate, a permission they
80// lack, or a field they got wrong; only a thing that does not exist has
81// no page to go back to.
82func (s *Server) done(w http.ResponseWriter, r *http.Request, code int, msg string,
83 redirect func(http.ResponseWriter, *http.Request, string)) {
84 switch code {
85 case protocol.ExitOK:
86 redirect(w, r, "")
87 case protocol.ExitNotFound:
88 s.notFound(w, r)
89 default:
90 redirect(w, r, msg)
91 }
92}
93
94// runControlStdin is runControl for the handful of commands whose input
95// arrives on stdin: public keys, and review comment bodies. Stdin is
96// also where a secret goes when one is set through this path, since
97// argv is world-readable in /proc and the audit log keeps flag values.
98func (s *Server) runControlStdin(u store.User, argv []string, stdin string) (msg string, ok bool) {
99 msg, code := s.runControlStdinCode(u, argv, stdin)
100 return msg, code == protocol.ExitOK
101}
102
103func (s *Server) runControlStdinCode(u store.User, argv []string, stdin string) (msg string, code int) {
104 return s.runControlReader(u, argv, strings.NewReader(stdin))
105}
106
107// runControlReader is runControlStdinCode for a body too large to hold
108// as a string: the command reads it from stdin as a stream.
109func (s *Server) runControlReader(u store.User, argv []string, stdin io.Reader) (msg string, code int) {
110 var stdout, stderr bytes.Buffer
111 ctx := &control.Ctx{
112 User: u,
113 Source: control.SourceWeb,
114 Scope: "full",
115 Store: s.st,
116 Cfg: s.cfg,
117 Stdin: stdin,
118 Stdout: &stdout,
119 Stderr: &stderr,
120 ViaAPI: true,
121 }
122 code = control.Dispatch(ctx, argv)
123 m := strings.TrimSpace(stderr.String())
124 if m == "" {
125 m = strings.TrimSpace(stdout.String())
126 }
127 return m, code
128}
129
130// runControlInto runs a command in JSON mode and decodes its data into
131// target. Read handlers use it so the web renders exactly what the CLI
132// and the API return, rather than reaching past the registry into git.
133func (s *Server) runControlInto(u store.User, argv []string, target any) (msg string, ok bool) {
134 code, msg := s.dispatchInto(u, argv, target)
135 return msg, code == protocol.ExitOK
136}
137
138// runControlIntoCode is runControlInto for handlers that have to tell
139// "no such thing" from "that failed": a profile page 404s on the first
140// and errors on the second.
141func (s *Server) runControlIntoCode(u store.User, argv []string, target any) (code int, msg string) {
142 return s.dispatchInto(u, argv, target)
143}
144
145func (s *Server) dispatchInto(u store.User, argv []string, target any) (int, string) {
146 return s.dispatchIntoStdin(u, argv, "", target)
147}
148
149// dispatchIntoStdin is dispatchInto with a body on stdin, decoding the
150// command's named payload rather than a map (#126).
151func (s *Server) dispatchIntoStdin(u store.User, argv []string, stdin string, target any) (int, string) {
152 var stdout, stderr bytes.Buffer
153 ctx := &control.Ctx{
154 User: u,
155 Source: control.SourceWeb,
156 Scope: "full",
157 Store: s.st,
158 Cfg: s.cfg,
159 Stdin: strings.NewReader(stdin),
160 Stdout: &stdout,
161 Stderr: &stderr,
162 JSON: true,
163 ViaAPI: true,
164 }
165 code := control.Dispatch(ctx, argv)
166 var env struct {
167 Data json.RawMessage `json:"data"`
168 Error string `json:"error"`
169 }
170 json.Unmarshal(stdout.Bytes(), &env)
171 if code != protocol.ExitOK {
172 m := env.Error
173 if m == "" {
174 m = strings.TrimSpace(stderr.String())
175 }
176 return code, m
177 }
178 if len(env.Data) > 0 {
179 if err := json.Unmarshal(env.Data, target); err != nil {
180 return protocol.ExitFailure, "unreadable response"
181 }
182 }
183 return protocol.ExitOK, ""
184}
185
186// dispatchJSON runs a command in JSON mode with stdin and returns its exit
187// code and, on failure, the message. In JSON mode a failure is an envelope
188// carrying the message rather than stderr text, so both paths are read
189// from the same envelope. A handler that wants the payload uses
190// runControlInto, which decodes into the command's own type instead of a
191// map nothing type-checks.
192func (s *Server) dispatchJSON(u store.User, argv []string, stdin string) (code int, msg string) {
193 var stdout, stderr bytes.Buffer
194 ctx := &control.Ctx{
195 User: u,
196 Source: control.SourceWeb,
197 Scope: "full",
198 Store: s.st,
199 Cfg: s.cfg,
200 Stdin: strings.NewReader(stdin),
201 Stdout: &stdout,
202 Stderr: &stderr,
203 JSON: true,
204 ViaAPI: true,
205 }
206 code = control.Dispatch(ctx, argv)
207 var env struct {
208 Error string `json:"error"`
209 }
210 json.Unmarshal(stdout.Bytes(), &env)
211 if code != protocol.ExitOK {
212 m := env.Error
213 if m == "" {
214 m = strings.TrimSpace(stderr.String())
215 }
216 if m == "" {
217 m = "the command failed"
218 }
219 return code, m
220 }
221 return code, ""
222}
223
224// authorNames maps commit author addresses to account names for one
225// request. A commit carries whatever name git was configured with; when
226// the address is a verified address here, the account's own name is the
227// truthful one to show, and it links somewhere.
228type authorNames struct {
229 st *store.Store
230 cache map[string]string
231}
232
233func (s *Server) authorNames() *authorNames {
234 return &authorNames{st: s.st, cache: map[string]string{}}
235}
236
237// name returns the account name for an address, or the commit's own
238// author name when no account has verified it.
239func (a *authorNames) name(email, fallback string) string {
240 if email == "" {
241 return fallback
242 }
243 if got, ok := a.cache[email]; ok {
244 if got == "" {
245 return fallback
246 }
247 return got
248 }
249 name, _ := a.st.UsernameByVerifiedEmail(email)
250 a.cache[email] = name
251 if name == "" {
252 return fallback
253 }
254 return name
255}
256
257// account returns the account name behind an address, if any, so callers
258// can link the displayed name to a profile.
259func (a *authorNames) account(email string) (string, bool) {
260 if email == "" {
261 return "", false
262 }
263 if got, ok := a.cache[email]; ok {
264 return got, got != ""
265 }
266 name, _ := a.st.UsernameByVerifiedEmail(email)
267 a.cache[email] = name
268 return name, name != ""
269}
270
271// namedCommit is a listing commit plus the account behind its author
272// address, when there is one, so the name can link to a profile.
273type namedCommit struct {
274 gitutil.EntryCommit
275 User string
276}
277
278// namedCommits rewrites listing authors to account names where the
279// address is verified here.
280func (s *Server) namedCommits(m map[string]gitutil.EntryCommit) map[string]namedCommit {
281 names := s.authorNames()
282 out := make(map[string]namedCommit, len(m))
283 for k, c := range m {
284 user, _ := names.account(c.Email)
285 c.Author = names.name(c.Email, c.Author)
286 out[k] = namedCommit{EntryCommit: c, User: user}
287 }
288 return out
289}
290
291// namedTip does the same for the single commit above a tree listing.
292func (s *Server) namedTip(c gitutil.EntryCommit) namedCommit {
293 names := s.authorNames()
294 user, _ := names.account(c.Email)
295 c.Author = names.name(c.Email, c.Author)
296 return namedCommit{EntryCommit: c, User: user}
297}
298
299// webViewer is the account behind a page request, or the zero user when
300// the instance serves the web without accounts.
301func (s *Server) webViewer(r *http.Request) store.User {
302 if s.cfg.Web.Mode != "accounts" {
303 return store.User{}
304 }
305 return s.viewer(r)
306}