blob: fbae00aa48d7ebaf9457610d6267921a94e56f03 [file] [log] [blame]
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package statusserver contains the status server for the golang.org
// properties and build infrastructure.
package statusserver
import (
"io"
"log"
"net/http"
"sort"
"sync"
"time"
"golang.org/x/build/status"
)
// Handler is the status server handler.
// It expects to be mounted at /status.
type Handler struct {
mu sync.Mutex
items map[string]*monItem
sortedItems []string // sorted keys of the items map
}
func NewHandler() *Handler {
h := &Handler{
items: map[string]*monItem{
"pubsubhelper": &monItem{
goodFor: 10 * time.Minute,
},
},
}
h.updateSorted()
return h
}
// Register registers h in mux at /status.
func (h *Handler) Register(mux *http.ServeMux) {
mux.Handle("/status", mux)
}
// requires h.mu is held
func (h *Handler) getItemLocked(name string) *monItem {
mi, ok := h.items[name]
if !ok {
mi = new(monItem)
mi.goodFor = 10 * time.Minute // default
h.items[name] = mi
h.updateSorted()
}
return mi
}
// requires h.mu is held
func (h *Handler) updateSorted() {
h.sortedItems = nil
for name := range h.items {
h.sortedItems = append(h.sortedItems, name)
}
sort.Strings(h.sortedItems)
}
// monItem is an item that is monitored. It can be monitored
// externally from the server hosting this handler, or it can be
// self-reported, in which case it'll come in via /status/update.
//
// All state is guarded by h.mu.
type monItem struct {
goodFor time.Duration
lastUpdate time.Time
ok bool
errText string
warn string
}
func (mi *monItem) updateFromRequest(r *http.Request) {
// TODO: record state changes over time, so we have a history of the last
// N transitions.
mi.lastUpdate = time.Now()
if s := r.FormValue("state"); s == "ok" {
mi.ok = true
mi.errText = ""
} else {
mi.ok = false
mi.errText = s
}
mi.warn = r.FormValue("warn")
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
if path == "/status/update" {
h.handleUpdate(w, r)
return
}
io.WriteString(w, "ok\n")
}
func (h *Handler) handleUpdate(w http.ResponseWriter, r *http.Request) {
switch {
case r.TLS == nil:
http.Error(w, "request requires TLS", http.StatusBadRequest)
return
case r.Method != "POST":
http.Error(w, "request requires POST", http.StatusBadRequest)
return
case r.Header.Get("Content-Type") != "application/x-www-form-urlencoded":
http.Error(w, "request requires Content-Type x-www-form-urlencoded", http.StatusBadRequest)
return
case r.ContentLength < 0:
http.Error(w, "request requires explicit Content-Length", http.StatusBadRequest)
return
case r.ContentLength > 1<<20:
http.Error(w, "request Content-Length should not exceed 1 MiB", http.StatusBadRequest)
return
}
wantToken, err := status.UpdateToken()
if err != nil {
log.Printf("error: status.UpdateToken: %v", err)
http.Error(w, "failed to get status update token to validate against", 500)
return
}
if r.Header.Get("X-Status-Update-Token") != wantToken {
http.Error(w, "invalid X-Status-Update-Token value", http.StatusUnauthorized)
return
}
name := r.FormValue("name")
h.mu.Lock()
defer h.mu.Unlock()
mi := h.getItemLocked(name)
mi.updateFromRequest(r)
w.WriteHeader(http.StatusNoContent)
}