| // 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) |
| } |