package console import ( "context" "crypto/subtle" "log" "net/http" "net/url" "strings" "time" "github.com/carlosframework/platform/internal/release" ) type BrowserRestartCopy struct { AppTitle, AppLine, HostTitle, HostLine string Button, Success, Audit string RateLimited, Failed string } var approvedBrowserRestartCopy = BrowserRestartCopy{ AppTitle: "Restart all app instances?", AppLine: "Request a restart for this app's running instances. Sleeping instances stay asleep.", HostTitle: "Restart {host}?", HostLine: "Only {host} receives a restart request. If it is asleep, it remains asleep.", Button: "Restart", Success: "Restart requested for {target}.", Audit: "Restart requested for {target}.", RateLimited: "Too many restart requests. Try again later.", Failed: "The restart request failed. Try again.", } func (s *Server) requestAppRestart(ctx context.Context, acct, app string) error { return release.TouchRestartGen(ctx, s.cfg.Releases, acct, app) } func (s *Server) handleAppRestart(w http.ResponseWriter, r *http.Request) { s.handleBrowserRestart(w, r, "", false) } func (s *Server) handleInstanceRestart(w http.ResponseWriter, r *http.Request) { s.handleBrowserRestart(w, r, strings.TrimSpace(r.PathValue("host")), true) } func (s *Server) handleBrowserRestart(w http.ResponseWriter, r *http.Request, host string, scoped bool) { addr, raw, acct, role, ok := s.account(w, r) if !ok { return } if subtle.ConstantTimeCompare([]byte(r.FormValue("csrf")), []byte(raw)) != 1 { s.renderMessage(w, r, http.StatusForbidden, "Refused", "Cross-site request blocked.") return } app := r.PathValue("app") has, err := s.store.HasAppIn(acct.ID, app) if err != nil { s.renderMessage(w, r, http.StatusInternalServerError, "Error", "Could not read your apps. Please try again.") return } if !has { http.NotFound(w, r) return } if scoped && host == "" { http.NotFound(w, r) return } var scope release.RestartScope if host != "" { if s.cfg.Registry == "" { http.NotFound(w, r) return } reg, err := openRegistry(s.cfg.Registry) if err != nil { log.Printf("console: browser restart registry: %v", err) http.NotFound(w, r) return } routes, err := reg.List() if err != nil { log.Printf("console: browser restart routes: %v", err) http.NotFound(w, r) return } found := false for _, rt := range routes { if rt.Host == host && rt.App == app && release.RouteAccount(rt) == acct.Sqid && release.Adoptable(rt) { found = true break } } if !found { http.NotFound(w, r) return } scope = release.RestartScope{Kind: release.RestartScopeHost, Value: host} } words := approvedBrowserRestartCopy if !confirmed(r) { cv := &ConfirmView{Action: r.URL.Path, Fields: [][2]string{{"csrf", raw}}, CancelTab: "instances"} if host == "" { cv.Title, cv.Lines, cv.Button = words.AppTitle, []string{words.AppLine}, words.Button } else { cv.Title = strings.ReplaceAll(words.HostTitle, "{host}", host) cv.Lines, cv.Button = []string{strings.ReplaceAll(words.HostLine, "{host}", host)}, words.Button } s.renderConfirm(w, r, addr, raw, acct, role, app, cv) return } if !s.allowWrite(Principal{Kind: PrincipalUser, Address: addr}, time.Now()) { s.setFlash(w, "err", words.RateLimited) http.Redirect(w, r, "/a/"+url.PathEscape(acct.Sqid)+"/apps/"+url.PathEscape(app)+"/instances", http.StatusSeeOther) return } if s.cfg.Releases == nil { s.setFlash(w, "err", words.Failed) http.Redirect(w, r, "/a/"+url.PathEscape(acct.Sqid)+"/apps/"+url.PathEscape(app)+"/instances", http.StatusSeeOther) return } ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) defer cancel() var restartErr error if host == "" { restartErr = s.requestAppRestart(ctx, acct.Sqid, app) } else { restartErr = s.writeRestartScope(ctx, acct.Sqid, app, scope) } if restartErr != nil { log.Printf("console: browser restart %s/%s/%s: %v", acct.Sqid, app, host, restartErr) s.setFlash(w, "err", words.Failed) } else { target := app if host != "" { target = host } detail := strings.ReplaceAll(words.Audit, "{target}", target) success := strings.ReplaceAll(words.Success, "{target}", target) targetType := "app" if host != "" { targetType = "instance" } s.auditActivity(acct, app, addr, Activity{Action: "restart", TargetType: targetType, Target: target, Detail: detail}) s.bumpSerial(ctx) s.setFlash(w, "ok", success) } http.Redirect(w, r, "/a/"+url.PathEscape(acct.Sqid)+"/apps/"+url.PathEscape(app)+"/instances", http.StatusSeeOther) }