carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "context"
5 "crypto/subtle"
6 "errors"
7 "log"
8 "net/http"
9 "net/url"
10 "strings"
11 "time"
12
13 "github.com/carlosframework/platform/internal/provision"
14)
15
16// The browser door's answer on a read-only console (apiRetryNoKey's twin).
17const (
18 browserRetryNoKeyTitle = "Retry unavailable"
19 browserRetryNoKey = "No signing key on this host, so this console cannot request a retry."
20 // The flashes for errRetryUnsupported and errRetryUnsaved
21 // (apiRetryUnsupported's and apiRetryUnsaved's twins).
22 browserRetryUnsupported = "This instance's box is too old to take a retry. Update the box, then try again."
23 browserRetryUnsaved = "This instance's box has not saved its failure yet. Try again in a minute."
24)
25
26// handleInstanceRetry is the instance page's Retry (spec §9.2): the browser
27// twin of `carlos instances retry`, shaped like the restart action: session,
28// CSRF, the byte-identical 404, a confirmation page, then the write. It
29// reads and writes through the CLI door's helpers (retryHostBuild,
30// requestHostRetry), so the two cannot disagree about what is held, and
31// both sign the same grant.
32func (s *Server) handleInstanceRetry(w http.ResponseWriter, r *http.Request) {
33 addr, raw, acct, role, ok := s.account(w, r)
34 if !ok {
35 return
36 }
37 if subtle.ConstantTimeCompare([]byte(r.FormValue("csrf")), []byte(raw)) != 1 {
38 s.renderMessage(w, r, http.StatusForbidden, "Refused", "Cross-site request blocked.")
39 return
40 }
41 app := r.PathValue("app")
42 host := strings.TrimSpace(r.PathValue("host"))
43 has, err := s.store.HasAppIn(acct.ID, app)
44 if err != nil {
45 s.renderMessage(w, r, http.StatusInternalServerError, "Error", "Could not read your apps. Please try again.")
46 return
47 }
48 if !has || !provision.ValidHostShape(host) || s.cfg.Releases == nil {
49 http.NotFound(w, r)
50 return
51 }
52 if s.cfg.Key == nil {
53 // A retry is a grant the root key signs (spec §11): a read-only
54 // console asks nothing, not even the confirmation.
55 s.renderMessage(w, r, http.StatusServiceUnavailable, browserRetryNoKeyTitle, browserRetryNoKey)
56 return
57 }
58 back := "/a/" + url.PathEscape(acct.Sqid) + "/apps/" + url.PathEscape(app) + "/instances/" + url.PathEscape(host)
59 flash := func(kind, msg string) {
60 s.setFlash(w, kind, msg)
61 http.Redirect(w, r, back, http.StatusSeeOther)
62 }
63 ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
64 defer cancel()
65 held, err := s.retryHostBuild(ctx, acct, app, host)
66 switch {
67 case errors.Is(err, errRetryNoRecord):
68 http.NotFound(w, r)
69 return
70 case errors.Is(err, errRetryNotHeld):
71 flash("err", strings.ReplaceAll(swapCopy.NotHeld, "{host}", host))
72 return
73 case errors.Is(err, errRetryUnsupported):
74 flash("err", browserRetryUnsupported)
75 return
76 case errors.Is(err, errRetryUnsaved):
77 flash("err", browserRetryUnsaved)
78 return
79 case err != nil:
80 log.Printf("console: browser retry %s/%s/%s: %v", acct.Sqid, app, host, err)
81 flash("err", swapCopy.RetryFailed)
82 return
83 }
84 fill := strings.NewReplacer("{host}", host, "{build}", held.build)
85 line := swapCopy.RetryLine
86 if held.declined {
87 line = swapCopy.RetryLineDeclined
88 }
89 if !confirmed(r) {
90 s.renderConfirm(w, r, addr, raw, acct, role, app, &ConfirmView{
91 Action: r.URL.Path, Fields: [][2]string{{"csrf", raw}}, CancelURL: back,
92 Title: fill.Replace(swapCopy.RetryTitle), Lines: []string{fill.Replace(line)},
93 Button: swapCopy.RetryButton,
94 })
95 return
96 }
97 if !s.allowWrite(Principal{Kind: PrincipalUser, Address: addr}, time.Now()) {
98 flash("err", swapCopy.RetryLimited)
99 return
100 }
101 if _, err := s.requestHostRetry(ctx, acct.Sqid, app, host, held); err != nil {
102 log.Printf("console: browser retry %s/%s/%s: %v", acct.Sqid, app, host, err)
103 flash("err", swapCopy.RetryFailed)
104 return
105 }
106 s.auditActivity(acct, app, addr, Activity{Action: "retry", TargetType: "instance", Target: host, Detail: fill.Replace(swapCopy.Audit)})
107 flash("ok", fill.Replace(swapCopy.RetryDone))
108}
109