diff --git a/assets/status.tmpl b/assets/status.tmpl index 84b3231..2894668 100644 --- a/assets/status.tmpl +++ b/assets/status.tmpl @@ -11,7 +11,17 @@ {{ .Service.Name }} {{ .Service.Started }} -
+ +
+ + + +
+
+ + + +
@@ -31,4 +41,4 @@ -{{ template "footer" . }} \ No newline at end of file +{{ template "footer" . }} diff --git a/internal/bundled/GENERATED_bundled.go b/internal/bundled/GENERATED_bundled.go index 1d22f40..8dff3de 100644 --- a/internal/bundled/GENERATED_bundled.go +++ b/internal/bundled/GENERATED_bundled.go @@ -10,4 +10,4 @@ var assets = map[string][]byte{ var assets_0 = []byte("\n\n{{ .Hostname }} — gokrazy\n\n\n\n\n \n\n
\n") var assets_1 = []byte("\n
\n\n\n\n\n") var assets_2 = []byte("{{ template \"header\" . }}\n\n
\n
\n\n

services

\n\n\n\n\n\n\n\n{{ range $idx, $svc := .Services }}\n\n\n\n\n{{ end }}\n\n
pathlast log line
\n{{ $svc.Name }}\n{{ if restarting $svc.Started }}\nrestarting\n{{ end }}\n{{ if $svc.Stopped }}\nstopped\n{{ end }}\n\n{{ last $svc.Stdout.Lines $svc.Stderr.Lines }}\n
\n
\n
\n

memory

\n{{ megabytes (index .Meminfo \"MemTotal\") }} total, {{ megabytes (index .Meminfo \"MemAvailable\") }} available
\nresident set size (RSS) by service:\n
\n\n{{ with $rss := initRss }}\n
\n\ninit\n
\n{{ end }}\n\n{{ range $idx, $svc := .Services }}\n{{ with $rss := $svc.RSS }}\n
\n\n{{ baseName $svc.Name }}\n
\n{{ end }}\n{{ end }}\n
\n\nunaccounted\n
\n
\n
\n\n
\n\n\n

storage

\n\n{{ if eq .PermAvail 0 }}\nNo permanent storage mounted. To create a filesystem for permanent storage, plug the SD card into a Linux computer and, if your SD card is /dev/sdb, use mkfs.ext4 /dev/sdb4.\n{{ else }}\n{{ .PermDev }}: {{ gigabytes .PermTotal }} total, {{ gigabytes .PermUsed }} used, {{ gigabytes .PermAvail }} avail
\n{{ end }}\n\n

private network addresses

\n\n\n

public network addresses

\n\n\n\n
\n
\n\n{{ template \"footer\" . }}\n") -var assets_3 = []byte("{{ template \"header\" . }}\n\n
\n
\n\n\n\n\n\n\n\n\n\n\n\n
NameStartedActions
{{ .Service.Name }}{{ .Service.Started }}
\n\n

stdout

\n
\n  {{ range $idx, $line := .Service.Stdout.Lines -}}\n    {{ $line }}\n  {{ end }}\n  
\n\n

stderr

\n
\n  {{ range $idx, $line := .Service.Stderr.Lines -}}\n    {{ $line }}\n  {{ end }}\n  
\n
\n
\n\n{{ template \"footer\" . }}") +var assets_3 = []byte("{{ template \"header\" . }}\n\n
\n
\n\n\n\n\n\n\n\n\n\n\n\n
NameStartedActions
{{ .Service.Name }}{{ .Service.Started }}\n
\n \n \n \n
\n
\n \n \n \n
\n\n

stdout

\n
\n  {{ range $idx, $line := .Service.Stdout.Lines -}}\n    {{ $line }}\n  {{ end }}\n  
\n\n

stderr

\n
\n  {{ range $idx, $line := .Service.Stderr.Lines -}}\n    {{ $line }}\n  {{ end }}\n  
\n
\n
\n\n{{ template \"footer\" . }}\n") diff --git a/status.go b/status.go index a422e70..928e2a2 100644 --- a/status.go +++ b/status.go @@ -94,24 +94,38 @@ var statusTmpl = template.Must(template.Must(commonTmpls.Clone()).New("statusTmp func initStatus(services []*service) { http.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) { + token := xsrfTokenFromCookies(r.Cookies()) + if token == 0 { + // Only generate a new XSRF token if the old one is expired, so that + // loading a different form in the background doesn’t render the + // current one unusable. + token = xsrfToken() + } + + http.SetCookie(w, &http.Cookie{ + Name: "gokrazy_xsrf", + Value: fmt.Sprintf("%d", token), + Expires: time.Now().Add(24 * time.Hour), + HttpOnly: true, + }) + path := r.FormValue("path") - var svc *service - for _, s := range services { - if s.cmd.Path != path { - continue - } - svc = s - break + svc := findSvc(path) + if svc == nil { + http.Error(w, "service not found", http.StatusNotFound) + return } var buf bytes.Buffer if err := statusTmpl.Execute(&buf, struct { Service *service BuildTimestamp string Hostname string + XsrfToken int32 }{ Service: svc, BuildTimestamp: buildTimestamp, Hostname: hostname, + XsrfToken: token, }); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/supervise.go b/supervise.go index bf9b59f..5e00b2c 100644 --- a/supervise.go +++ b/supervise.go @@ -255,6 +255,21 @@ func stopstartHandler(w http.ResponseWriter, r *http.Request) { return } + cookieToken := xsrfTokenFromCookies(r.Cookies()) + if cookieToken == 0 { + http.Error(w, "XSRF cookie missing", http.StatusBadRequest) + return + } + i, err := strconv.ParseInt(r.FormValue("xsrftoken"), 0, 32) + if err != nil { + http.Error(w, fmt.Sprintf("parsing XSRF token form value: %v", err), http.StatusBadRequest) + return + } + if formToken := int32(i); cookieToken != formToken { + http.Error(w, "XSRF token mismatch", http.StatusForbidden) + return + } + signal := syscall.SIGTERM if r.FormValue("signal") == "kill" { signal = syscall.SIGKILL @@ -266,7 +281,7 @@ func stopstartHandler(w http.ResponseWriter, r *http.Request) { http.Error(w, "no such service", http.StatusNotFound) return } - var err error + if r.URL.Path == "/restart" { err = restart(s, signal) } else { diff --git a/xsrf.go b/xsrf.go new file mode 100644 index 0000000..217492e --- /dev/null +++ b/xsrf.go @@ -0,0 +1,42 @@ +package gokrazy + +import ( + cryptorand "crypto/rand" + "encoding/binary" + "log" + "math/rand" + "net/http" + "strconv" + "sync" +) + +func xsrfTokenFromCookies(cookies []*http.Cookie) int32 { + for _, c := range cookies { + if c.Name != "gokrazy_xsrf" { + continue + } + if i, err := strconv.ParseInt(c.Value, 0, 32); err == nil { + return int32(i) + } + } + return 0 +} + +// lazyXsrf is a lazily initialized source of random numbers for generating XSRF +// tokens. It is lazily initialized to not block early boot when reading +// cryptographically strong random bytes to seed the RNG. +var lazyXsrf struct { + once sync.Once + rnd *rand.Rand +} + +func xsrfToken() int32 { + lazyXsrf.once.Do(func() { + var buf [8]byte + if _, err := cryptorand.Read(buf[:]); err != nil { + log.Fatalf("lazyXsrf: cryptorand.Read: %v", err) + } + lazyXsrf.rnd = rand.New(rand.NewSource(int64(binary.BigEndian.Uint64(buf[:])))) + }) + return lazyXsrf.rnd.Int31() +}